【进阶算法】前缀和

前缀和是指数组某个索引之前的所有元素的和,是一种重要的代码技巧,使用前缀和可以快速求出数组某一个区间的和。

 

一、一维数组的前缀和

示例:数组 arr = [8,1,3,-2,5,0,-3,6],输入 m 个询问,每个询问输入一对 [L , R]。对于每个询问,要求输出原数组中从第 L 个数到第 R 个数的和。

比如,第 1 次询问,输入 [0, 2],需要输出 12;第 2 次询问,输入 [2, 5],需要输出 6;第 3 次询问,输入 [0, 6],需要输出 12。

这个问题可以很容易的通过遍历数组解决,但是每次都需要遍历数组,时间复杂度比较高。如果使用前缀和数组,可以大大提高运算效率。

 

1.1 前缀和数组

定义一个前缀和数组 preSum[arr.len + 1],保存原数组 arr 每个元素的前缀和,其中 preSum[i] = arr[i - 1] 的前缀和,也就是前缀和数组与原数组相比,下标向右偏移一位。根据前缀和定义可知,preSum[i] = preSum[i - 1] + arr[i - 1]。

这样,区间 [L, R] 的和就等于 preSum[R +1] - preSum[L]。原理如下:

preSum[R + 1] = arr[0] + arr[1] + arr[2] + ... + arr[L - 1] + arr[L] + arr[L + 1] + ... + arr[R - 1] + arr[R]
preSum[L] = arr[0] + arr[1] + arr[2] + ... + arr[L - 1] 
preSum[R + 1] - preSum[L] = arr[L] + arr[L + 1] + ... + arr[R - 1] + arr[R]

 

1.2 代码实现

// 原数组
int[] arr = {8, 1, 3, -2, 5, 0, -3, 6};
// 前缀和数组
int[] preSum = preSum(arr);

/**
 * 构造前缀和数组
 *
 * @param arr 原数组
 * @return 前缀和数组
 */
private int[] preSum(int[] arr) {
    int len = arr.length;
    int[] preSum = new int[len + 1];
    for (int i = 1; i <= len; i++) {
        preSum[i] = preSum[i - 1] + arr[i - 1];
    }
    return preSum;
}

/**
 * 获取数组闭区间 [left, right] 的累加和
 *
 * @param left  区间左边界
 * @param right 区间右边界
 * @return 数组闭区间 [left, right] 的累加和
 */
public int sumRange(int left, int right) {
    return preSum[right + 1] - preSum[left];
}

 

二、前缀和适用场景

前缀和数组的适用场景:

  原始数组不会被修改的情况下,频繁查询某个区间的累加和。

 

三、二维数组的前缀和

示例:有个 m * n 的整数矩阵,输入 q 个询问,每个询问包含 4 个整数 (x1, y1) (x2, y2),表示一个子矩阵的左上角坐标和右下角坐标,要求输出每个询问的子矩阵中所有元素的和。

比如,矩阵 matrix = [[3, 0, 1, 4, 2], [5, 6, 3, 2, 1], [1, 2, 0, 1, 5], [4, 1, 0, 1, 7], [1, 0, 3, 0, 5]],输入 (2, 1) (4, 3),红色框表示的子矩阵,输出 8;输入 (1, 1) (2, 2),黄色框表示的子矩阵,输出 11。

 

这个问题同样可以通过遍历解决,但是使用前缀和数组效率更高。

 

3.1 二维前缀和数组

定义一个前缀和矩阵 preSum[m + 1][n + 1],保存原矩阵 matrix 每个元素的前缀和,其中 preSum[i][j] = matrix[i - 1][j - 1] 的前缀和,也就是前缀和矩阵与原矩阵相比,下标向右偏移一位、向下偏移一位。根据前缀和定义可知,preSum[i][j] = preSum[i][j - 1] + preSum[i - 1][j] - preSum[i - 1][j - 1] + matrix[i - 1][j - 1],图示如下(i、j表示原矩阵的下标):

这样,子矩阵 (x1, y1) (x2, y2) 的和就等于 preSum[x2 + 1][y2 + 1] - preSum[x2 + 1][y1] - preSum[x1][y2 + 1] + preSum[x1][y1],原理如下:

3.2 代码实现

// 原矩阵
int[][] matrix = {{3, 0, 1, 4, 2}, {5, 6, 3, 2, 1}, {1, 2, 0, 1, 5}, {4, 1, 0, 1, 7}, {1, 0, 3, 0, 5}};
// 前缀和矩阵
int[][] preSum = preSum(matrix);

/**
 * 构造前缀和矩阵
 *
 * @param matrix 原矩阵
 * @return 前缀和矩阵
 */
private int[][] preSum(int[][] matrix) {
    int row = matrix.length;
    int col = matrix[0].length;
    int[][] preSum = new int[row + 1][col + 1];

    for (int i = 1; i <= row; i++) {
        for (int j = 1; j <= col; j++) {
            preSum[i][j] = preSum[i][j - 1] + preSum[i - 1][j] - preSum[i - 1][j - 1] + matrix[i - 1][j - 1];
        }
    }
    return preSum;
}

/**
 * 获取子矩阵 (x1, y1) (x2, y2) 的元素和
 *
 * @param x1 子矩阵左上角的横坐标
 * @param y1 子矩阵左上角的纵坐标
 * @param x2 子矩阵右下角的横坐标
 * @param y2 子矩阵右下角的纵坐标
 * @return 子矩阵 (x1, y1) (x2, y2) 的元素和
 */
public int sumRange(int x1, int y1, int x2, int y2) {
    return preSum[x2 + 1][y2 + 1] - preSum[x2 + 1][y1] - preSum[x1][y2 + 1] + preSum[x1][y1];
}

 

四、练习题目

1. 统计考试分数段内的学生数量

问题:假如某班级有若干同学,每个同学有一个期末考试成绩(满分 100 分),请你实现一个 API,输入任意一个分数段,返回有多少同学的成绩在这个分数段内。

思路:可以先统计每个分数具体有多少个同学,然后利用前缀和技巧来实现分数段查询的 API。

int[] scores; // 每个学生的分数
int[] count = new int[100 + 1]; // 满分100分

// 统计每个分数对应的学生数量
for(int score : scores){
    count[score]++;
}

int[] preSum = new int[101 + 1]; // count数组的前缀和
for(int i = 1; i < preSum.length; i++){
    preSum[i] = preSum[i - 1] + count[i - 1];
}

// 利用 preSum 获取分数区间的学生数量
preSum[R + 1] - preSum[L];

 

LeetCode 303. 区域和检索 - 数组不可变

LeetCode 1991. 找到数组的中间位置

LeetCode 304. 二维区域和检索 - 矩阵不可变

 

posted @ 2023-11-04 16:18  有点成长  阅读(222)  评论(0编辑  收藏  举报