前缀和
1概念
顾名思义,前缀和就是数组当前位置前的所有位置之和,一般用于动态规划的题中,理解前缀和对某些题就可以直接套用模板来做了。
2 初始化
在一维数组中,比如[1, 2, 3, 4]的前缀和树组就是[0, 1, 3, 6, 10];在二维中同理前缀和数组要比给定数组长度大一,这是方便写转移方程。
3 应用场景
在一维数组中,常常用于求数组中索引从i到j的总和,sumRange(i, j)= sum[j + 1] - sum[i],即索引i到j的和等于前j + 1个元素的和减去前i个元素的和,
二维数组中的前缀和,常常用于求子矩阵范围内元素的总和。
先求二维的前缀和如下图,来自官方题解:
然后求sumRegion(row1,col1,row2,col2) = sum[row2 + 1] [col2 + 1] - sum[row1] [col2 + 1] - sum[row2 + 1] [col1] + sum[row1] [col1]
3 前缀和的一些题目
560. 和为K的子数组 |
---|
303. 区域和检索 - 数组不可变 |
304. 二维区域和检索 - 矩阵不可变 |
363. 矩形区域不超过 K 的最大数值和 |
4 代码
560.和为K的子数组
class Solution {
/*
思路:求出前缀和数组,然后双重遍历该数组,统计k出现的次数
*/
public int subarraySum(int[] nums, int k) {
int n = nums.length;
// 1. 前缀和数组
int[] f = new int[n + 1];
for (int i = 1; i <= n; i++) {
f[i] = f[i - 1] + nums[i - 1];
}
// 求出f[j] - f[i] = k的个数,其中j>i,即我们要找到f[i] = f[j] - k出现的次数(注:因为有可能出现负数,因此满足f[i]的个数不止一个)
// map中存放键表示当前的前缀和,值表示当前前缀和出现的次数。找到满足条件的f[i]后,更新count=count + map.getOrDefault(f[j]-k, 0) + 1
int count = 0;
Map<Integer, Integer> map = new HashMap<>();
map.put(0, 1); // 初始化前缀和为0出现的次数为1,因为count初始化是0,因此碰到满足的f[i]时要将count变成1
for (int i = 1; i <= n; i++) {
if (map.containsKey(f[i] - k)) { // 如果map中存在满足条件的键,更新count加上出现的次数
count += map.get(f[i] - k);
}
map.put(f[i], map.getOrDefault(f[i], 0) + 1);
}
return count;
}
}
优化:根据上面的代码我们可以看出来前缀和的计算只与前一项的答案有关,因此可以优化掉f数组。
class Solution {
public int subarraySum(int[] nums, int k) {
int n = nums.length;
int count = 0, pre = 0;
Map<Integer, Integer> map = new HashMap<>();
map.put(0, 1);
for (int i = 0; i < n; i++) {
pre += nums[i];
if (map.containsKey(pre - k)) {
count += map.get(pre - k);
}
map.put(pre, map.getOrDefault(pre, 0) + 1);
}
return count;
}
}
303.区域和检索-区域不可变
class NumArray {
private int[] sums;
public NumArray(int[] nums) {
int n = nums.length;
sums = new int[n + 1];
for (int i = 0; i < n; i++) {
sums[i + 1] = sums[i] + nums[i];
}
}
public int sumRange(int i, int j) {
return sums[j + 1] - sums[i];
}
}
304.二维区域和检索-矩阵不可变
class NumMatrix {
int[][] sums;
public NumMatrix(int[][] matrix) {
int m = matrix.length;
if (m > 0) {
int n = matrix[0].length;
sums = new int[m + 1][n + 1];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
sums[i + 1][j + 1] = sums[i + 1][j] + sums[i][j + 1] - sums[i][j] + matrix[i][j];
}
}
}
}
public int sumRegion(int row1, int col1, int row2, int col2) {
return sums[row2 + 1][col2 + 1] - sums[row1][col2 + 1] - sums[row2 + 1][col1] + sums[row1][col1];
}
}
- 矩形区域不超过 K 的最大数值和
class Solution {
/*
思路一:二维前缀和枚举所有情况
1. 求出所有的前缀和(模板)
2. 枚举所有情况,即以左上角和右下角为移动点进行全盘扫描,四重循环。
*/
public int maxSumSubmatrix(int[][] matrix, int k) {
int m = matrix.length, n = matrix[0].length;
int[][] sum = new int[m + 1][n + 1];
// 1. 求前缀和
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + matrix[i - 1][j - 1];
}
}
int ans = Integer.MIN_VALUE;
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
for (int p = i; p <= m; p++) {
for (int q = j; q <= n; q++) {
int cur = sum[p][q] - sum[i - 1][q] - sum[p][j - 1] + sum[i - 1][j - 1];
if (cur <= k) {
ans = Math.max(cur, ans);
}
}
}
}
}
return ans;
}
}
优化:二分优化
class Solution {
public int maxSumSubmatrix(int[][] matrix, int k) {
int ans = Integer.MIN_VALUE;
int m = matrix.length, n = matrix[0].length;
for (int i = 0; i < m; ++i) { // 枚举上边界
int[] sum = new int[n];
for (int j = i; j < m; ++j) { // 枚举下边界
for (int c = 0; c < n; ++c) {
sum[c] += matrix[j][c]; // 更新每列的元素和
}
TreeSet<Integer> sumSet = new TreeSet<Integer>();
sumSet.add(0);
int s = 0;
for (int v : sum) {
s += v;
Integer ceil = sumSet.ceiling(s - k);
if (ceil != null) {
ans = Math.max(ans, s - ceil);
}
sumSet.add(s);
}
}
}
return ans;
}
}