【进阶算法】前缀和
前缀和是指数组某个索引之前的所有元素的和,是一种重要的代码技巧,使用前缀和可以快速求出数组某一个区间的和。
一、一维数组的前缀和
示例:数组 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];
本文来自博客园,作者:有点成长,转载请注明原文链接:https://www.cnblogs.com/luwei0424/p/17809271.html