前缀和算法

一、一维前缀和

给定一个数组: a 1 , a 2 , a 3 , . . . a n a_1, a_2, a_3, ... a_n a1,a2,a3,...an

前缀和数组: s 1 , s 2 , s 3 , s 4 , . . . s n s_1, s_2, s_3, s_4,... s_n s1,s2,s3,s4,...sn

如何求前缀和数组: s i = a 1 + a 2 + a 3 + . . . + a i s_i = a_1 + a_2 + a_3 + ... + a_i si=a1+a2+a3+...+ai

for (int i = 1; i <= n; i++) s[i] = s[i - 1] + a[i];

作用:快速地求出某段区间的和

给你一个区间[l, r],求出该区间的和,只需要s[r] - s[l - 1]

s[r] = a[1] + a[2] + a[3] + ... a[l - 1] + a[l] + ... + a[r]

s[l-1] = a[1] + a[2] + a[3] + ... + a[l - 1]

s[r] - s[l - 1] = a[l] + a[l + 1] + ... + a[r]

定义:s[0] = 0

因为当l = 1时,求[l, r] => [1, r]

此时s[0] = 0, 所以s[r] - s[l - 1] = s[r] - s[0] = s[r]

公式仍然成立

二、一维前缀和总结

一维前缀和可以通过O(n)的预处理,在O(1)的时间内求出某个区间的和

三、一维前缀和题目

  • 303. 区域和检索 - 数组不可变 - 力扣(LeetCode)
  • 560. 和为 K 的子数组 - 力扣(LeetCode)

四、题目参考代码

// 303 Java代码
class NumArray {
    int[] s;
    public NumArray(int[] nums) {
        int n = nums.length;
        s = new int[n + 1];
        for (int i = 1; i <= n; i++) 
            s[i] = s[i - 1] + nums[i - 1];
    }
    
    public int sumRange(int left, int right) {
        return s[right + 1] - s[left];
    }
}

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray obj = new NumArray(nums);
 * int param_1 = obj.sumRange(left,right);
 */

// 560 Java参考代码
class Solution {
    public int subarraySum(int[] nums, int k) {
        int n = nums.length;
        int[] s = new int[n + 1];
        for (int i = 1; i <= n; i++) {
            s[i] = s[i - 1] + nums[i - 1];
        }
        int ans = 0;
        Map<Integer, Integer> map = new HashMap<>();
        map.put(0, 1);
        for (int r = 1; r <= n; r++) {
            int t = s[r];
            if (map.containsKey(t - k)) ans += map.get(t - k);
            map.put(t, map.getOrDefault(t, 0) + 1);
        }
        return ans;
    }
}

五、二维前缀和

原数组: a [ 1 ] [ 1 ] , a [ 1 ] [ 2 ] , . . . , a [ 1 ] [ m ] , a [ 2 ] [ 1 ] , a [ 2 ] [ 2 ] , . . . , a [ n ] [ m ] a[1][1], a[1][2], ..., a[1][m], a[2][1], a[2][2], ..., a[n][m] a[1][1],a[1][2],...,a[1][m],a[2][1],a[2][2],...,a[n][m]

前缀和数组: s [ 1 ] [ 1 ] , s [ 1 ] [ 2 ] , s [ 1 ] [ 3 ] , . . . , s [ n ] [ m ] s[1][1], s[1][2], s[1][3], ..., s[n][m] s[1][1],s[1][2],s[1][3],...,s[n][m]

// 求前缀和数组
for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
        s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
    }
}

// 求出任意区间的和[x1, y1],[x2, y2]
s = s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1];

六、二维前缀和总结

二维前缀和可以通过O(nm)的预处理,在O(1)的时间内求出某个区间的和

七、二维前缀和题目

  • 304. 二维区域和检索 - 矩阵不可变 - 力扣(LeetCode)
  • 363. 矩形区域不超过 K 的最大数值和 - 力扣(LeetCode)

八、二维前缀和题目参考代码

// 304 Java代码
class NumMatrix {
    int[][] s;
    public NumMatrix(int[][] matrix) {
        int n = matrix.length, m = matrix[0].length;
        s = new int[n + 1][m + 1];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + matrix[i - 1][j - 1];
            }
        }
    }
    
    public int sumRegion(int row1, int col1, int row2, int col2) {
        row1++;
        col1++;
        row2++;
        col2++;
        return s[row2][col2] - s[row2][col1 - 1] - s[row1 - 1][col2] + s[row1 - 1][col1 - 1];
    }
}

/**
 * Your NumMatrix object will be instantiated and called as such:
 * NumMatrix obj = new NumMatrix(matrix);
 * int param_1 = obj.sumRegion(row1,col1,row2,col2);
 */

// 363 Java代码
class Solution {
    public int maxSumSubmatrix(int[][] matrix, int k) {
        int n = matrix.length, m = matrix[0].length;
        int[][] s = new int[n + 1][m + 1];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + matrix[i - 1][j - 1];
            }
        }
        int ans = Integer.MIN_VALUE;
        
        for (int x2 = 1; x2 <= n; x2++) {
            for (int x1 = 1; x1 <= x2; x1++) {
                TreeSet<Integer> map = new TreeSet<>();
                map.add(0);
                for (int y2 = 1; y2 <= m; y2++) {
                    int t = sumRegion(s, x1, 1, x2, y2);
                    Integer x = map.ceiling(t - k);
                    if (x != null) {
                        ans = Math.max(ans, t - x);
                    }
                    
                    map.add(t);
                }
            }
        }
        return ans;
    }

    public int sumRegion(int[][] s, int row1, int col1, int row2, int col2) {
        return s[row2][col2] - s[row2][col1 - 1] - s[row1 - 1][col2] + s[row1 - 1][col1 - 1];
    }
}

九、总结

通过知识点,我们可以猜出出题人会怎么考?

既然前缀和能快速的求出某段区间的和,那如果我不给你区间呢?我反过来直接给你某个区间和的值,但是我不告诉你是哪个区间,让你求出该区间,这时你会怎么做?

聪明的你应该想到了,那我就暴力枚举 l l l r r r,这时就需要 O ( n 2 ) O ( n^2 ) O(n2) 的时间复杂度,如果出题人给出一个 1 0 5 10^5 105 量级的计算量,那么你就超时啦。

这时你最多枚举 l l l 或者 r r r ,另一个最多给你 O ( l o g n ) O(logn) O(logn) 是时间复杂度枚举,这时你就应该往二分或者哈希表上面去想了,挖掘出题目中的某些信息,从而求出答案。

写在最后

这里我只是抛砖隐喻,告诉各位不要刷太多题,多站在出题人的角度去思考问题,往往一个知识点也就那样了。
前缀和算法_第1张图片

你可能感兴趣的:(算法,java,笔记,经验分享,leetcode,数据结构)