算法-01-前缀和与差分

120 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

前缀和与差分

又开了一个算法新坑,不仅仅希望可以记录自己学习的过程,还希望可以帮助到更多的小伙伴更好地去学习算法,本节将两个比较相似的算法一起来写一下。(相关的例题都会在Leetcode中,具体的题面可以去那看^v^)

前缀和

前缀和是用一个数组S[i]记录一段数据Nums[i]前i个数之和

S[i] = S[i - 1] + Nums[i]

同时可得子段和为

sum(l,r) = S[r] - S[l - 1]

例题1:LeetCode1248 统计优美子数组
class Solution {
    public int numberOfSubarrays(int[] nums, int k) {
        int[] s = new int[nums.length + 1];
        for (int i = 1; i < nums.length + 1; i++) {
            s[i] = s[i - 1] + nums[i - 1] % 2;
        }
        int[] count = new int[nums.length + 1];
        count[0] = 1;
        int res = 0;
        for (int i = 1; i < nums.length + 1; i++) {
            if (s[i] - k < 0) {
                count[s[i]]++;
                continue; 
            }
            res += count[s[i] - k];
            count[s[i]]++;
        }
        return res;
    }
}

本题其实最容易能够想到的方法应该是先把所有的数据都变成1和0,再统计前缀和,得到前缀和数组后用两个for循环去计算子段和,如果子段和等于k的话,那么这个子段就是符合条件的。但是时间复杂度还是比较高,达到了O(n^2)。

我们是根据等式

S[i] - S[j] = k

如果符合该等式的话res++,换句话说如果我们是两重循环我们是在遍历S[j],如果有一个S[j]符合条件,结果就会加一,我们把等式稍微变化一下

S[j] = S[i] - k

那么我们就找到了S[j]和S[i]、k之间的关系,这意味着我们可以提前把S[j]的数量找到,找到S[j]的数量后我们就不用再去加一个循环遍历S[j]。直接根据上面的关系就可以求得S[j]。

这个遍历S[i]是从前往后,所以后面的S[j]情况并不会影响到前面的计数,所以S[j]的统计可以和遍历S[i]同步进行。

类似题目:LeetCode 560

例题2:LeetCode 53 最大子数组和

class Solution {
    public int maxSubArray(int[] nums) {
        if (nums.length == 1) return nums[0];
        int[] sum = new int[nums.length + 1];
        sum[0] = nums[0];
        for (int i = 1; i <= nums.length; i++) {
            sum[i] = sum[i - 1] + nums[i - 1];
        }
        int res = Integer.MIN_VALUE;
        int preMin = sum[0];
        for (int i = 1; i <= nums.length; i++) {
            res = Math.max(res, sum[i] - preMin);
            preMin = Math.min(preMin, sum[i]);
        }
        return res;
    }
}

这道题其实刚好契合了求子段和的公式,但是如果需要用两个for循环的话时间复杂度也比较高了。由于这个只需要记录最大的子数组和,并且在遍历前缀和数组的时候只需要考虑前面数组的前缀和,与后面的数组无关。

因此我们只需要记录一个当前的最小前缀和,用当前的前缀和减去最小前缀和就可以得到当前遍历到的数之前最大的子数组和是多少。

那么这道题的解法就是先算出来数组的前缀和数组,再遍历前缀和数组,计算子数组和并且改变当前的最小前缀和。

例题3:LeetCode304 二维区域和检索 - 矩阵不可变

class NumMatrix {
    int[][] preSum;

    public NumMatrix(int[][] matrix) {
        preSum = new int[matrix.length][matrix[0].length];
        preSum[0][0] = matrix[0][0];
        for (int i = 1; i < matrix[0].length; i++) {
            preSum[0][i] = matrix[0][i] + preSum[0][i - 1];
        }
        for (int i = 1; i < matrix.length; i++) {
            preSum[i][0] = preSum[i - 1][0] + matrix[i][0];
        }
        for (int i = 1; i < matrix.length; i++) {
            for (int j = 1; j < matrix[0].length; j++) {
                preSum[i][j] = preSum[i - 1][j] + preSum[i][j - 1] - preSum[i - 1][j - 1] + matrix[i][j];
            }
        }
    }
    
    public int sumRegion(int row1, int col1, int row2, int col2) {
        return getSum(row2, col2) - getSum(row2, col1 - 1) - getSum(row1 - 1, col2) + getSum(row1 - 1,col1 - 1);
    }

    public int getSum(int i, int j) {
        if (i >= 0 && j >= 0) return preSum[i][j];
        return 0;
    }
}

这道题同样是前缀和相关的问题,但是从一维上升到了二维。分析一下二维的结构

截屏2022-12-04 15.36.46.png

由图可得公式

S[i][j] = S[i][j - 1] + S[i - 1][j] - S[i - 1][j - 1] + Nums[i][j]

在加左边和上边的前缀和后需要减去重复的左上角前缀和,同时可得子矩阵图

截屏2022-12-04 16.09.08.png

由图可得公式

sum(p,q,i,j) = S[i][j] - S[i][q - 1] - S[p - 1][j] + S[p - 1][q - 1]

那得到这个公式后问题就迎刃而解了,在初始化时把前缀和数组计算完毕后按照上面的式子求子矩阵和就可以了。

差分

差分的概念是相邻相减,如下面一组数

原数组:1, 2, 6, -3, -1, 3

差分数组:1, 1, 4, -9, -2, 4

可得差分数组的公式为

B[i] = Nums[i] - Nums[i - 1]

这个差分数组还有一个很特殊的性质,如果对差分数组求前缀和那么就得到了原数组

利用这个很特别的性质可以解决某些题目

例题:LeetCode1109 航班预订统计

class Solution {
    public int[] corpFlightBookings(int[][] bookings, int n) {
        int[] record = new int[n];
        for (int i = 0; i < bookings.length; i++) {
            record[bookings[i][0] - 1] += bookings[i][2];
            if (bookings[i][1] >= n) continue;
            record[bookings[i][1]] -= bookings[i][2];
        }
        for (int i = 1; i < n; i++) {
            record[i] += record[i - 1];
        }
        return record;
    }
}

这道题其实最容易想的方法应该是用两个for循环遍历航班,并更新记录数组,最后再对记录数组求和。但是这样时间复杂度为O(m*n)。我们可以进行进一步的优化。

根据我们上面提到的差分数组求前缀和可以得到原数组,这里我们考虑用差分数组减少遍历的时间。比如说实例1输入的第一个数据[1, 2, 10]这个意味着第一个和第二个航班加十个座位,那么记录到差分数组中就应该是B[0] + 10, B[1]不用变,因为B[1]和B[0]都是+10的,意味着他们之间的差没有改变,但是到B[2]的时候应该-10,由于第二个航班增加了十个座位,那么第三个和第二个之间就差了十,根据实例1可得差分数组和前缀和:

初始化:[0, 0, 0, 0, 0]

第一组:[+10, 0, -10, 0, 0]

第二组:[+10, +20, -10, -20, 0]

第三组:[+10, +45, -10, -20, 0]

前缀和:[10, 55, 45, 25, 25]

由此前缀和便是需要求的航班座位数。

总结

前缀和与差分通常用来解决的都是特定的一类问题,通常是求子段和,以及[l,r]的数据可以由[1,r]以及[1,l - 1]得到,满足区间减法的题目。

并且有的题目需要两个一起使用才能解决,我们要记住一个性质就是

前缀和--求差分-->原数组

差分--前缀和-->原数组

感谢阅读!