前言:
二维前缀和是一维前缀和在二维上的体现,要想了解清楚二维前缀和,最好先学习一维前缀和,在逐步深入二维前缀和。因此我们先从一维的开始讲起,另外一个容易忽视的点是,前缀和优化的是查询复杂度,而不是总体的复杂度,请注意这个区别。
一维前缀和
我们先从问题引入其概念:
给定一个整数数组nums,求数组从索引i到j(i≤j)范围内元素的总和。
这个问题我们可以很容易的想到一个O(n)的解决方案,从数组左端开始扫描,在扫描索引到i开始,用一个变量依次与每个元素相加,直到指针扫过j。
但是有没有更快的方法呢,而一维前缀和就是来解决这一类的问题,它的时间复杂度是O(1),我们创建一个数组f,f[i]表示nums[0:i - 1]之和,例如:nums[0]=1,nums[1]=2,那么f[2]=3,f[i]中的i我们可以把它比作nums数组前i个数字相加。而f[0]显然等于0.因此对于有n个数的数组nums,我们可以设置一个大小为n+1的数组f。f[j + 1]-f[i],就表示索引i到j的元素总和。
一维前缀和用处有很多,比如它是计数排序和基数排序(前者的进阶)的核心。
代码实现:
class NumArray {
//sums[i]表示前nums[0],...,nums[i-1]之和
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];
}
}
二维前缀和的引入
现在我们知道一维前缀和的含义及基本使用场合,我们现在来回答下一个问题。
给定一个二维矩阵,计算其子矩形范围内元素的总和,该子矩阵的左上角为 (row1, col1) ,右下角为 (row2, col2) 。
解析:暴力的做法是我们依次遍历row1到row2行,并用一个变量来存储结果。这样我们每次查询的时间复杂度达到了O(mn)。但我们仔细想一想,其实这个问题我们也可以用一维前缀和来解决,每一个矩阵其实都是一排排的一维数组组成的,因此我们可以为每一排创建一维的前缀和数组f,每行的和用f计算,这样我们每次查询的时间复杂度就降到了O(m),但是要考虑的时我们初始化前缀和数组需要O(mn),因为一共有m排需要分别初始化。
代码实现:
class NumMatrix {
int[][] sums;
public NumMatrix(int[][] matrix) {
int m = matrix.length;
if (m > 0) {
int n = matrix[0].length;
sums = new int[m][n + 1];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
sums[i][j + 1] = sums[i][j] + matrix[i][j];
}
}
}
}
public int sumRegion(int row1, int col1, int row2, int col2) {
int sum = 0;
for (int i = row1; i <= row2; i++) {
sum += sums[i][col2 + 1] - sums[i][col1];
}
return sum;
}
}
从前面可知一维的前缀和做法并没有使查询的效率达到O(1),那有没有做法达到呢?这时我们就有了今天的主题,二维前缀和。我们可以想象成一维干一维的事,二维干二维的事。
我们通过类比,定义f(i,j) 为矩阵matrix的以(i,j)为右下角的子矩阵的元素之和。但我们在实际设置这个数组时,f[i,j]的含义设为有i行,j列的矩阵的元素之和,而每次查询的行列参数都是下标值(从0开始),所以在使用f数组时需要加1,这一点很重要。
那在f数组的含义下,我们怎么计算f数组呢?
当i=0或j=0时,f[i,j]显然等于0
当i或j都大于0时,我们假设计算f[i,j]时已经知道了f[i-1,j],f[i,j-1]和f[i-1,j-1]的值,那么我们可以通过一张图来知道我们怎么利用他们
从图可以自己推导出:f(i,j)=f(i−1,j)+f(i,j−1)−f(i−1,j−1)+matrix[i][j]
计算查询结果
而对于每次查询sumRegion(row1,col1,row2,col2),当row1=0且col1=0时,则退化成了计算f[row1+1][col1+1]
的值。
在一般情况,当row1≤row2且col1≤col2时,我们可以通过下面这张图看出计算过程:
有了上述的推导,我们即可在O(1)时间内得到sumRegion(row 1, col1 , row2 , col2)的值。
代码实现部分:
class NumMatrix {
public:
vector<vector<int>> sums;
NumMatrix(vector<vector<int>>& matrix) {
int m = matrix.size();
if (m > 0) {
int n = matrix[0].size();
sums.resize(m + 1, vector<int>(n + 1));
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
sums[i + 1][j + 1] = sums[i][j + 1] + sums[i + 1][j] - sums[i][j] + matrix[i][j];
}
}
}
}
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];
}
};
下面我们来看在使用二维前缀和的基础上,如何去解决一个问题。
给你一个 m x n 的矩阵 matrix 和一个整数 k ,找出并返回矩阵内部矩形区域的不超过 k 的最大数值和。
从题目中我们可以通过枚举矩阵左上角和右下角(即枚举每个可能的子矩形),通过二维前缀和数组算出每个矩阵和是否满足条件。这是一种朴素的二维前缀和方法。
代码实现:
class Solution {
public int maxSumSubmatrix(int[][] matrix, int k) {
int m = matrix.length, n = matrix[0].length;
int[][] sum = new int[m + 1][n + 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++) {//左上角(i,j)
for (int p = i; p <= m; p++) {
for (int q = j; q <= n; q++) {//右下角(p,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(ans, cur);
}
}
}
}
return ans;
}
}
但这种方法容易超时,且我们有优化的方案。
但是由于优化方法需要二分+有序集合的知识,我们下次说。