• Leetcode__1508. Range Sum of Sorted Subarray Sums


    Leetcode__1508. Range Sum of Sorted Subarray Sums

    这一题是一道十分典型的二分查找与双指针的使用,题目的大意是,给出一个数组 (nums) , 我们可以通过该数组得到子数组, 子数组的大小从 1 到 (n), 因此有 (frac{n(n+1)}{2}) 个不同的子数组, 然后我们要求这些子数组的和, 然后对这些子数组的和进行排序, 求排序后的数组的区间和, 那么这道题的结果就是一些子数组和 的和, 可能有一点拗口, 我们会用例子说明.

    我们定义原数组为 (nums), 他的 (frac{n(n+1)}{2}) 个子数组的和 构成新的有序数组为 (SUM_{sub}) , 例如 (nums = [1,2,3,4]), 那么我们可以计算得到 (SUM_{sub} = [1, 2, 3, 3, 4, 5, 6, 7, 9, 10]) , 然后我们要在 (SUM_{sub}) 上求区间和, 传统的方式就是求得 (SUM_{sub}), 然后在区间 ([left: right]) 上求和.

    问题思路转换

    我们要将定义重新捋一下, (SUM_{sub})子数组的和, 并且 (SUM_{sub}) 是有序的, 那么 (SUM_{sub}) 的前缀和(Prefix_{subsum}[k] =sum_{i=0}^k SUM_{sub}[i])就表示在子数组的和当中的最小的 (k) 个元素之和, 我们要求的是 (SUM_{sub}[left])(SUM_{sub}[right]) 这部分的和, 也就等于 (Prefix_{subsum}[right] - Prefix_{subsum}[left-1]),

    现在思考一个问题, 如果找到了第 (k) 大的子数组和 (SUM_k) , 能否在(O(n))时间内找到小于 (SUM_k) 的子数组的和.

    方法是: 对于原数组 (nums), 遍历下标 (0-i), 我们计算最大的 (j), 使得 (nums) 在区间([i:j]) 上的和 小于 (SUM_k), 那么在区间 ([i:j]) 内的所有以 (i) 为起始点的子数组 的子数组和 都小于 (SUM_k). (注意,这里下标 (j) 是包括, 官方题解是不包括, 当然, 这是个小问题)

    例如 (nums = [1,2,3,4]), 如果我们知道了第 (8) 大的子数组和是 (7), 那么我们可以遍历 ([1,2,3,4]) 的下标, 从下标为 (0) 开始, 下标为(0)(j)(2), 下标 (1) 对应的(j)(2), 那么就得到子数组和小于(6) 的子数组集合为 ({[1],[1,2],[1,2,3],[2],[2,3],[3],[4] }), 然后我们求这些子数组的和 的和就是 (Prefix_{subsum}[7]). 注意, 这里是 (Prefix_{subsum}[7]) 而不是 (8) 因为我们求的是严格小于 (SUM_8) 的子数组和, 上面的集合中, 刚好就是 (7) 个元素.

    优化一下计算的方式

    上述过程, 在求得 (SUM_k) 之后, 我们可以计算得到子数组和小于 (SUM_k) 的子数组集合的区间是: ({[0,0_j], [1 : 1_j], ..[m : m_j]}), 对于其中一个区间 ([m: m_j]) 来说, 一共有 (m_j - m +1) 个子数组, 现在我们需要优化计算这 (m_j - m +1) 个子数组和的和, 例如, 在上面的例子中, 对于 (m==0) 的时候, 子数组就是 ([1])([1,2]), 子数组和 的和就是 (4).

    计算方式就是引入前缀和, 定义 (Prefix_{sum})(nums) 的前缀和, (Prefix\_Prefix_{sum})(Prefix_{sum}) 的前缀和, 也就是 (nums) 前缀和的前缀和, 那么区间 ([i;j]) 上的所有以 (i) 为起始下标的 (j-i+1) 个子数组和的和为:

    [Prefix\_Prefix_{sum}[j]−Prefix\_Prefix_{sum}[i-1]−Prefix_{sum}[i-1]×(j−i+1) ]

    这里的(Prefix\_Prefix_{sum}[j]) 表示的是 (Prefix_{sum}) 的前缀和, (包括 (j)) , (Prefix\_Prefix_{sum}[j]−Prefix\_Prefix_{sum}[i-1]) 表示的是 (sum_{k = i}^{j} Prefix_{sum}[k]) , 但是这并不等于 ([i: j]) 上子数组和的和, 因为(Prefix_{sum}) 是前缀和, 那么会把 (nums[0]+ nums[1]...+nums[i-1]) 也计算在内, 计算的次数就是 (j-i+1) 次, 所以我们要减去 (Prefix_{sum}[i-1]).

    例如

    (nums = [1,2,3,4]), 那么

    (Prefix_{sum})([1,3,6,10]),

    (Prefix\_Prefix_{sum})([1,4,10,20]),

    对于区间 ([2: 3]), (Prefix\_Prefix_{sum}[3]−Prefix\_Prefix_{sum}[1]) 等于 (9), 表示 (Prefix_{sum}) 数组中 (3, 6) 的和, 那么对于原数组来说 (Prefix_{sum}[0]) 就被多加进去了, 所以要删去, 删去的次数就是两次.

    特殊情况((i== 0))

    (i== 0) 时, (Prefix\_Prefix_{sum}[j]) 就是区间 ([0,0_j]) 上的子数组和的和, 不需要减去后半部分.

    返回结果

    我们已经计算了区间 ([i: j]) 上的(j-i+1) 个子数组和 的和, 回头看, 我们已知子数组和小于 (SUM_k) 的子数组集合的区间是: ({[0,0_j], [1 : 1_j], ..[m : m_j]}), 对于每个区间, 我们都使用上述的计算方式, 计算每个区间内的子数组和 的和. 我们就得到了整个 (nums) 数组中, 所有的 子数组和 小于(SUM_k) 的这些子数组和 的和. 换句话说, 就是 (sum_{i= 0}^{k_{low}}SUM_i) , 注意, 我们的计算方式并不是计算每个 (SUM_i), 而是用子区间的方式计算所有 (SUM_i) 的和.

    还原假设

    我们刚才的计算步骤都基于一个假设, 那就是 如果找到了第 (k) 大的子数组和 (SUM_k) , 现在我要做的就是如何找到 (SUM_k), 因此, 这一题的思路就是,

    1. 使用二分法找到 (SUM_{right})(SUM_{left-1}), 注意, 这里的 (SUM_k) 是第 (k) 大, 而不是前 (k) 大的和,
    2. 根据 (SUM_k), 计算前 (k) 大的和. 也就是计算前 (right) 大的和, 与 前 (left-1) 大的和.

    二分法求 (SUM_k)

    (SUM_k)的计算方式本质上和 计算小于 (SUM_k) 的区间的计算方式相同, 只不过, 这里我们是需要计算 (k), 而不是计算子数组和的和. 也就是说, 对于 (Middle), 我们要判断, 他是不是第 (k) 大, 那么我们计算 子数组和 小于 (Middle) 的子数组的个数 (Middle_{count}) 就可以了, 如果这个(Middle_{count}) 大于 (k), 那么我们就要缩小 (Middle), 如果 (Middle_{count}) 小于 (k), 我们就要增大 (Middle), 缩小与增大的方式就使用 二分法.

    具体怎么求 (Middle) 对应的 (Middle_{count}) 呢, 本质上也是计算区间集合, 同样, 我们遍历 (i)(0)((n-1)), 以(i) 为下标, 计算最大的 (j), 使得 (sum_{k=i}^{j} nums[k] <= Middle) ,也就是计算不超过 (Middle) 的子数组和对应的 (j). 注意,这里是小于等于, 前面讲的是严格小于. 那么从 (i)(j) 一共有 (j-i+1) 个子数组. 遍历之后, 我们会得到子数组和小于 (Middle) 的子数组集合的区间是: ({[0,0_j], [1 : 1_j], ..[m : m_j]}), 也可以计算 子数组和 小于等于 (Middle) 的子数组的总个数 (Middle_{count}).

    注意(): 与「378. 有序矩阵中第K小的元素」十分类似的一个问题就是, 假设我们得到一个 (Middle) ,满足小于等于 (Middle) 的子数组和的个数是 (k) 个, 但是(Middle) 自身并不是子数组和, 举例来说, 还是对于 (nums = [1,2,3,4]). 它的(SUM_{sub} = [1, 2, 3, 3, 4, 5, 6, 7, 9, 10]) , 那么 (Middle) 等于 (8)的时候和 (Middle) 等于 (7) 的时候, (Middle_{count}) 都是 (8), 所以, 我们需要限定二分法找 (Middle) 的时候, 当(Middle_{count}) 大于等于 (k), 那么我们就要缩小 (Middle), 使得 (right = Middle), 而不是 (Middle -1), 具体的思想可以参考「378. 有序矩阵中第K小的元素」.

    计算子数组和的前缀和

    子数组和的前缀和也就是根据 (SUM_k), 计算前 (k) 大的(SUM_i) 的和, 刚才, 我们已经计算得到了(Middle), 但是可能有多个 (Middle) 大小的子数组和, 所以我们需要将 (sum_{i= 0}^{k}SUM_i) 分成两部分,

    一部分是严格小于 (Middle)(SUM_i), 这部分的和, 我们刚才分析的时候已经计算过了, 需要注意的时候, 在计算的过程中, 还需要统计 (SUM_i) 的次数, 假设是 (count) 次, 这些子数组和 的和用 (SUM_1) 表示.

    另一部分是等于 (Middle)(SUM_i), 也就是说 (SUM_i = SUM_k). 这部分的子数组的个数就是 (k-count). 这一部分的结果就是 ((k-count)* Middle).

    所以总共的子数组和的和就是: (SUM_1 + (k-count)* Middle). 这一部分就是子数组和的前缀和. 也就是 (sum_{i= 0}^{k}SUM_i) .

    那么最后的答案就是 (sum_{i= left}^{right}SUM_i = sum_{i=0}^{right}SUM_i - sum_{i=0}^{left-1}SUM_i)

    最后, 贴一下实现的代码:

    class Solution {
    public:
        static constexpr int MODULO = 1000000007;
        int rangeSum(vector<int>& nums, int n, int left, int right) {
            // 计算我们使用到的前缀和
            vector<int> prefixSums = vector<int>(n);
            prefixSums[0] = nums[0];
            for (int i = 1; i < n; i++) {
                prefixSums[i] = prefixSums[i - 1] + nums[i];
            }
            vector<int> prefixPrefixSums = vector<int>(n);
            prefixPrefixSums[0] = prefixSums[0];
            for (int i = 1; i < n; i++) {
                prefixPrefixSums[i] = prefixPrefixSums[i - 1] + prefixSums[i];
            }
            // 分别计算子数组和排序后的 的前缀和, 第right个 和 第left-1个
            return (getSum(prefixSums, prefixPrefixSums, n, right) - getSum(prefixSums, prefixPrefixSums, n, left - 1)) % MODULO;
        }
    
        int getSum(vector<int>& prefixSums, vector<int>& prefixPrefixSums, int n, int k) {
            // 如果 k==0 表示left == 1, 相当于left小于最小的子数组和, 所以为 0
            if(k == 0) {
                return 0;
            }
            // 计算第 k 大的子数组和
            int num = getKth(prefixSums, n, k);
            int sum = 0;
            int count = 0;
            // 计算小于 num, 在上述文章中是 Middle 的所有子数组的和
            for (int i = 0, j = 0; i < n; i++) {
                int temp = 0;
                j = i;
                if (i > 0) {
                    temp = prefixSums[i - 1];
                }
                while (j < n && prefixSums[j]-temp < num) {
                    j++;
                }
                // 只有 j>i 表示子数组的大小至少为 1的时候, 我们加入新的子数组的和
                if(j > i) {
                    j--;
                    if (i > 0) {
                        sum += prefixPrefixSums[j] - prefixPrefixSums[i - 1] - prefixSums[i - 1] * (1 + j - i);
                    }
                    else {
                        sum += prefixPrefixSums[j];
                    }
                    sum %= MODULO;
                    count += (j - i + 1);
                }
            }
            // 有 count 个子数组和严格小于 Middle, 剩下的(k-count) 等于 Middle
            sum += num * (k - count);
            return sum;
        }
    
        int getKth(vector<int>& prefixSums, int n, int k) {
            int low = 0, high = prefixSums[n - 1];
            // 得到第 k 大的子数组和, 注意这里的二分法, 最后返回 low, 
            while (low < high) {
                int mid = (high - low) / 2 + low;
                int count = getCount(prefixSums, n, mid);
                if (count < k) {
                    low = mid + 1;
                }
                else {
                    high = mid;
                }
            }
            return low;
        }
    
        int getCount(vector<int>& prefixSums, int n, int x) {
            // 对于给定的 x, 计算他是第几大的子数组和
            int count = 0;
            for (int i = 0, j = 0; i < n; i++) {
                int temp = 0;
                if (i > 0) {
                    temp = prefixSums[i - 1];
                }
                while (j < n && prefixSums[j]-temp <= x) {
                    j++;
                }
                count += (j - i);
            }
            return count;
        }
    };
    
  • 相关阅读:
    《游戏改变世界》笔记
    2017第6周日
    换个角度看执行力
    怎样拥有执行力和高效能
    提高个人执行力的五个关键
    Apache服务器部署多个进程
    IE的Cookie目录和临时缓存目录的关系
    IE/Firefox/Chrome等浏览器保存Cookie的位置
    在浏览器中简单输入一个网址,解密其后发生的一切(http请求的详细过程)
    如何设置win7系统的文件夹为系统文件,从而隐藏文件夹
  • 原文地址:https://www.cnblogs.com/wevolf/p/15201318.html
Copyright © 2020-2023  润新知