题目
给定一个非空二维矩阵 matrix 和一个整数 k,找到这个矩阵内部不大于 k 的最大矩形和。
示例:
输入: matrix = [[1,0,1],[0,-2,3]], k = 2
输出: 2
解释: 矩形区域 [[0, 1], [-2, 3]] 的数值和是 2,且 2 是不超过 k 的最大数字(k = 2)。
说明:
矩阵内的矩形区域面积必须大于 0。
如果行数远大于列数,你将如何解答呢?
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/max-sum-of-rectangle-no-larger-than-k
解题
思路(固定左右边界求矩形最大和将其转化为一维数组最大子序和问题)
对matrix = [[5,-4,-3,4],[-3,-4,4,5],[5,1,5,-4]]的矩阵来说,如下图,要选择其中子矩阵的小于K的最大矩阵和。最直观的思路就是枚举所有的(左边界,右边界)和(上边界,下边界),然后求这四个边界内的元素的和,和的求法可以是每一行相加也可以是每一列相加。
暴力法来说,循环一般写成先枚举一对边界的组合,再枚举另一对边界的组合。因为枚举一对边界就是两层循环,这样下来是四层循环。我们要想办法简化问题。
我们可以固定一对边界(因为行数远大于列数,所以先枚举固定左右边界),然后不用枚举另一种边界的组合可以通过这样的方式来完成:固定左右边界后,上下边界组合下的矩阵的和我们可以通过该左右边界下每一行的和的方式来计算。
所以我们枚举不同的左右边界组合,对每一种左右边界来说,求该边界条件下其每一行的和用rowsum[row.length]存储。这样枚举上下边界并求该上下左右边界构成的矩形最大和的方式就变成了求rowsum[]数组的最大子序和的问题。这样既完成了上下边界的枚举,又相当于将二维数组压缩成了一维数组来求解。如下图示例,当左右边界为[0,2]时,rowsum[] = {-2,-3,11}。问题就变成求解rowsum数组的小于K的最大子序和的求解了。如下给出了几种解法。
此时的矩形和求解方法例如下面这种直接求法:
rowsum[]的小于K的最大子序和解法
暴力求法
枚举所有情况,对连续的子数组进行依次累加维护小于K的最大值即可。
class Solution {
public int maxSumSubmatrix(int[][] matrix, int k) {
//固定左右边界,求出该左右边界内每行的和用rowsum[]存储,只考虑哪两个行之间的矩形和最大,就变成了求rowsum中最大子序和的问题
//1、划定左右边界,求每行和;2、找不超过K的两行的组合(即对应矩阵)的最大值
//为什么用列划分,因为行数远超于列数
int row = matrix.length;//行数
//非空矩阵不用判断特殊情况
int col = matrix[0].length;//列树
int res = Integer.MIN_VALUE;//初始化结果为最小值
for(int l = 0; l < col;l++){//l为左边界
int[] rowsum = new int[row];//每一行的和的数组
for(int r = l; r < col; r++){//r为有边界
for(int i = 0; i < row; i++){
//求当前左右边界[l,r]时每行的和的前缀和,以便求不同行之间的矩形和
rowsum[i] += matrix[i][r];
}
//求[l,r]边界下矩阵不超过k的最大矩形和
res = Math.max(res,lrmax(rowsum,k));
if(res == k) return k;//尽量减少运算
}
}
return res;
}
//求[l,r]边界下矩阵不超过k的最大矩形和,变成rowsum数组的小于K的最大子序和问题
public int lrmax(int[] rowsum, int k){
//暴力法 O(row^2)
int max = Integer.MIN_VALUE;
for(int i = 0; i < rowsum.length; i++){
int sum = 0;
for(int j = i; j < rowsum.length;j++){
//i到j行之间的和就是依次叠加
sum += rowsum[j];
if(sum<=k && sum>max) max = sum;
if(max == k) return k;
}
}
return max;
}
}
前缀和求法
在小于K的最大子序和的求解上,采用前缀和,前缀和详解具体看leetcode【前缀和】 | 560. 和为K的子数组
class Solution {
public int maxSumSubmatrix(int[][] matrix, int k) {
//固定左右边界,求出该左右边界内每行的和用rowsum[]存储,只考虑哪两个行之间的矩形和最大,就变成了求rowsum中最大子序和的问题,用前缀和求(参考"leetcode 560. 和为K的子数组")
//1、划定左右边界,求每行和;2、找不超过K的两行的组合(即对应矩阵)的最大值
//为什么用列划分,因为行数远超于列数
int row = matrix.length;//行数
//非空矩阵不用判断特殊情况
int col = matrix[0].length;//列树
int res = Integer.MIN_VALUE;//初始化结果为最小值
for(int l = 0; l < col;l++){//l为左边界
int[] rowsum = new int[row];//每一行的和的数组
for(int r = l; r < col; r++){//r为有边界
for(int i = 0; i < row; i++){
//求当前左右边界[l,r]时每行的和的前缀和,以便求不同行之间的矩形和
rowsum[i] += matrix[i][r];
}
//求[l,r]边界下矩阵不超过k的最大矩形和
res = Math.max(res,lrmax(rowsum,k));
if(res == k) return k;//尽量减少运算
}
}
return res;
}
//求[l,r]边界下矩阵不超过k的最大矩形和
public int lrmax(int[] rowsum, int k){
//前缀和求法 参考最大子序和那题
int max = Integer.MIN_VALUE;
//计算前缀和rowsum数组的
int[] presum = new int[rowsum.length];
presum[0] = rowsum[0];
for(int i = 1; i < rowsum.length;i++){
presum[i] = presum[i-1]+rowsum[i];
}
//通过前缀和来求两行之间最大的矩形和
for(int i = 0; i < rowsum.length; i++){
int sum = 0;
for(int j = i; j < rowsum.length;j++){
//i到j行之间的和就是依次叠加
sum = presum[j]-presum[i]+rowsum[i];
if(sum<=k && sum>max) max = sum;
if(max == k) return k;
}
}
return max;
}
}
动态规划求法(最大子序和解法 最优)
参考最大子序和的动态规划求法,以dp来表示以i为结尾的数组元素之和,若dp小于0为负数就不必要再加上前面的连续的(因为加上的话对后面的和也起不到会更大的作用),重新从i+1开始重新算dp。
因为此题多了一个在不大于K的情况下最大值的求法,所以上述动态规划解法求出的最大值是不限制K时求得的。当K>无限制时求得的最大值时没有任何问题上述结果就是本题结果;但是当K<无限制的最大值时,就会出现因为中间dp<0时都不与后面相加而导致的一些情况的漏算,这些漏算的情况下有可能会存在<=K时的最大值。所以我们需要额外判断K的取值范围,当K<我们所求无限制max时,还是得用上述暴力/前缀和解法把所有情况枚举出来判断。但是这样还是节省了很多时间。
class Solution {
public int maxSumSubmatrix(int[][] matrix, int k) {
//固定左右边界,求出该左右边界内每行的和用rowsum[]存储,只考虑哪两个行之间的矩形和最大,就变成了求rowsum中小于K的最大子序和的问题
//1、划定左右边界,求每行和;2、找不超过K的两行的组合(即对应矩阵)的最大值
//为什么用列划分,因为行数远超于列数
int row = matrix.length;//行数
//非空矩阵不用判断特殊情况
int col = matrix[0].length;//列树
int res = Integer.MIN_VALUE;//初始化结果为最小值
for(int l = 0; l < col;l++){//l为左边界
int[] rowsum = new int[row];//每一行的和的数组
for(int r = l; r < col; r++){//r为有边界
for(int i = 0; i < row; i++){
//求当前左右边界[l,r]时每行的和的前缀和,以便求不同行之间的矩形和
rowsum[i] += matrix[i][r];
}
//求[l,r]边界下矩阵不超过k的最大矩形和
res = Math.max(res,lrmax(rowsum,k));
if(res == k) return k;//尽量减少运算
}
}
return res;
}
//求[l,r]边界下矩阵不超过k的最大矩形和
public int lrmax(int[] rowsum, int k){
//动态规划参考最大子序和那题
int max = Integer.MIN_VALUE;
int dp = 0;//维护当前元素为结尾的之前所有连续子数组的最大的和
for(int num:rowsum){
dp = dp>0?dp+num:num;
max = Math.max(dp,max);
if(max == k) return k;
}
//因为有不大于k的限制,所以可能会因为dp<0断开而漏掉一些值
if(max<=k) return max;//如果k在已求得最大值上,就不存在上述问题
//如果k比无限制时求得的max小,就要重新把所有情况计算下来了 这里采用暴力法
max = Integer.MIN_VALUE;
for(int i = 0; i < rowsum.length; i++){
int sum = 0;
for(int j = i; j < rowsum.length;j++){
//i到j行之间的和就是依次叠加
sum += rowsum[j];
if(sum<=k && sum>max) max = sum;
if(max == k) return k;
}
}
return max;
}
}
前缀和+二分法
前缀和求两行间矩形元素和的方法就是sum = presum[j]-presum[i] = presum[大] -presum[小]
我们要求的sum = presum[大] - presum[小] <= k的sum最大值。
我们将presum[大] - presum[小]<= k换成 presum[小] >= presum[大] - k,就可以变成:当presum[大]固定时,求最小的presum[小] 使sum结果趋近于k。
找presum[小]的话,就要固定presum[大],并将presum[大]前的所有presum都排序,然后二分查找最小的满足presum[小] >= presum[大] - k的presum[小]。
class Solution {
public int maxSumSubmatrix(int[][] matrix, int k) {
//固定左右边界,求出该左右边界内每行的和,只考虑那两个行之间的句型和最小,用前缀和求
//1、划定左右边界,求每行和;2、找不超过K的两行的组合(即对应矩阵)的最大值
//为什么用列划分,因为行数远超于列数
int row = matrix.length;//行数
//非空矩阵不用判断特殊情况
int col = matrix[0].length;//列树
int res = Integer.MIN_VALUE;//初始化结果为最小值
for(int l = 0; l < col;l++){//l为左边界
int[] rowsum = new int[row];//每一行的和的数组
for(int r = l; r < col; r++){//r为有边界
for(int i = 0; i < row; i++){
//求当前左右边界[l,r]时每行的和的前缀和,以便求不同行之间的矩形和
rowsum[i] += matrix[i][r];
}
//求[l,r]边界下矩阵不超过k的最大矩形和
res = Math.max(res,lrmax(rowsum,k));
if(res == k) return k;//尽量减少运算
}
}
return res;
}
//求[l,r]边界下矩阵不超过k的最大矩形和
public int lrmax(int[] rowsum, int k){
//前缀和+二分 求presum[i] >= presum[j]-k的最小presum[i] 对j之前的i的presum[i]排序并进行二分查找
int[] presum = new int[rowsum.length+1];
presum[0] = 0;
for(int i = 0; i < rowsum.length;i++){
presum[i+1] = presum[i]+rowsum[i]; //这样sum[i到j] = presum[j+1]-presum[i]
}
int max = rowsum[0]<=k?rowsum[0]:Integer.MIN_VALUE;
//以下的j为rowsum中的行数,在presum中对应的下标为j+1
for(int j = 2; j < rowsum.length; j++){
//对j之前的presum[i]存储起来prearr[],j之前一共有j+1个presum并排序(j的presum使presum[j+1],包含0有j+2个元素)
int[] prearr = Arrays.copyOf(presum,j+1);
Arrays.sort(prearr);
int target = presum[j+1];
if(target- prearr[j]>k) continue;
//在prearr里找最小的满足小>=target-k的小,用二分法,prearr中一共有j+1个元素
int left = 0, right = j;
while(left<right){
int mid = left+(right-left)/2;
if(prearr[mid]<target-k){
left = mid+1;
}else{
right = mid;
}
}
int small = prearr[right]>=target-k?prearr[right]:prearr[left];
//因为可能这个左右边界的所有矩形都不符合矩形元素和小于k,所以还要加上判断
max = target-small<=k?Math.max(target-small,max):max;
if(max == k) return k;
}
return max;
}
}
上述代码剪枝优化: 可以看出用前缀和的求法时间消耗很大,可以运用上一种解法中的动态规划解法进行剪枝,对k>无限制求出的最大值还是用上述动态规划解法,其他情况再用这种解法求全部的情况,剪枝代码如下:
class Solution {
public int maxSumSubmatrix(int[][] matrix, int k) {
//固定左右边界,求出该左右边界内每行的和,只考虑那两个行之间的句型和最小,用前缀和求
//1、划定左右边界,求每行和;2、找不超过K的两行的组合(即对应矩阵)的最大值
//为什么用列划分,因为行数远超于列数
int row = matrix.length;//行数
//非空矩阵不用判断特殊情况
int col = matrix[0].length;//列树
int res = Integer.MIN_VALUE;//初始化结果为最小值
for(int l = 0; l < col;l++){//l为左边界
int[] rowsum = new int[row];//每一行的和的数组
for(int r = l; r < col; r++){//r为有边界
for(int i = 0; i < row; i++){
//求当前左右边界[l,r]时每行的和的前缀和,以便求不同行之间的矩形和
rowsum[i] += matrix[i][r];
}
//求[l,r]边界下矩阵不超过k的最大矩形和
res = Math.max(res,lrmax(rowsum,k));
if(res == k) return k;//尽量减少运算
}
}
return res;
}
//求[l,r]边界下矩阵不超过k的最大矩形和
public int lrmax(int[] rowsum, int k){
//动态规划+ 前缀和&二分 求presum[i] >= presum[j]-k的最小presum[i] 对j之前的i的presum[i]排序并进行二分查找
int max = Integer.MIN_VALUE;
//剪枝 k>无限制最大值时的情况直接用最快的dp法
int dp = 0;//维护当前元素为结尾的之前所有连续子数组的最大的和
for(int num:rowsum){
dp = dp>0?dp+num:num;
max = Math.max(dp,max);
if(max == k) return k;
}
//因为有不大于k的限制,所以可能会因为dp<0断开而漏掉一些值
if(max<=k) return max;
//k<无限制max时的求解,这里采用前缀和+二分法
int[] presum = new int[rowsum.length+1];
presum[0] = 0;
for(int i = 0; i < rowsum.length;i++){
presum[i+1] = presum[i]+rowsum[i]; //这样sum[i到j] = presum[j+1]-presum[i]
}
max = rowsum[0]<=k?rowsum[0]:Integer.MIN_VALUE;
//以下的j为rowsum中的行数,在presum中对应的下标为j+1
for(int j = 2; j < rowsum.length; j++){
//对j之前的presum[i]存储起来prearr[],j之前一共有j+1个presum并排序(j的presum使presum[j+1],包含0有j+2个元素)
int[] prearr = Arrays.copyOf(presum,j+1);
Arrays.sort(prearr);
int target = presum[j+1];
//因为可能这个左右边界的所有矩形都不符合矩形元素和小于k,所以还要先判断将这个情况排除
if(target- prearr[j]>k) continue;
//在prearr里找最小的满足小>=target-k的小,用二分法,prearr中一共有j+1个元素
int left = 0, right = j;
while(left<right){
int mid = left+(right-left)/2;
if(prearr[mid]<target-k){
left = mid+1;
}else{
right = mid;
}
}
int small = prearr[right]>=target-k?prearr[right]:prearr[left];
max = Math.max(target-small,max);
if(max == k) return k;
}
return max;
}
}
参考:
[1] java从暴力开始优化,配图配注释
[2] 固定左右边界,前缀和+二分