363. 矩形区域不超过 K 的最大数值和 - 力扣(LeetCode)
第一感觉是和LeetCode第 862 题:和至少为K的最短子数组 (C++)_qq_32523711的博客-CSDN博客比较类似,而且矩阵元素和子数组都有连续的特点,所以这题可以先计算前缀和试一试。
暴力解
不过还是先看看暴力解吧,要注意题目提示的行数远大于列数,方法很简单,固定左右(列)边界,这样就得到了一个贯穿头尾的(窄)矩形,计算该矩形内每一行的和存储在一个vector内,最后在vector中遍历(记住矩阵内的元素可以为负),不断记录并更新符合要求的最大数值和就行,这样就计算完了一个窄矩阵(一对左右边界)。
下面更新左右边界即可。
记住help函数的vector参数最好用引用,可以大量节省空间。
class Solution {
public:
int maxSumSubmatrix(vector<vector<int>>& matrix, int k) {
int row = matrix.size();
int col = matrix[0].size();
int result = INT_MIN; //最小的int值
// 行数远大于列数
// left, right为左右边界
for(int left=0; left < col; ++left){
vector<int> sum(row, 0); //用于存储该矩形的行的和
for(int right=left; right < col; ++right){
for(int i=0; i < row; ++i)
sum[i] += matrix[i][right];
result = max(result, helper(row, sum, k));
}
}
return result;
}
// 用于计算数值和
int helper(const int row, const vector<int> &vec, int k){
int max_sum = INT_MIN;
for(int i=0; i < row; ++i){
int sum = 0;
for(int j = i; j < row; ++j){
sum += vec[j];
if(sum <= k && sum > max_sum) max_sum = sum;
}
}
return max_sum;
}
};
好像暴力解结果看着也还行:
优化
因为矩阵中可能有负数的存在,所以help函数
中当没有找到最大值的时候,是有必要一直遍历到vector结尾的,但是如果提前找到的数值和sum
== k
的话,就可以立即返回了,因为可以确定已经是最大了。
对主函数中的result
也是如此。
class Solution {
public:
int maxSumSubmatrix(vector<vector<int>>& matrix, int k) {
int row = matrix.size();
int col = matrix[0].size();
int result = INT_MIN; //最小的int值
// 行数远大于列数
// left, right为左右边界
for(int left=0; left < col; ++left){
vector<int> sum(row, 0); //用于存储该矩形的行的和
for(int right=left; right < col; ++right){
for(int i=0; i < row; ++i)
sum[i] += matrix[i][right];
result = max(result, helper(row, sum, k));
if(result == k) return result;
}
}
return result;
}
// 用于计算数值和
int helper(const int row, const vector<int> &vec, int k){
int max_sum = INT_MIN;
for(int i=0; i < row; ++i){
int sum = 0;
for(int j = i; j < row; ++j){
sum += vec[j];
if(sum == k) return sum;
if(sum < k && sum > max_sum) max_sum = sum;
}
}
return max_sum;
}
};
不过结果来看这点小优化掀不起什么波澜。
接着优化:
我们的目的是,在众多矩形块中不断地累加他们的值,使得和值越接近k
越好,越快越好!
在help函数
中,当sum
累加的时候:
如果时刻t有:sum < 0
,sum
为当前已累加的值,那么接着累加下去必然会使得后面那部分更小:可以理解为当前小于0的sum只会拖累我们继续向目标(更快地接近k
)前进,那就不如舍弃k
,然后在接下来的位置从`0``开始累加。
注意k可以小于0.。。
把k想象成山顶,sum < 0就相当于爬山的时候掉到了悬崖底下。
参考这个题解
暴力+O(n)优化+二分+二分剪枝 - 矩形区域不超过 K 的最大数值和
不过这个思路总感觉怪怪的,加的那一段是否有效和测试数据关系很大。
class Solution {
public:
int maxSumSubmatrix(vector<vector<int>>& matrix, int k) {
int row = matrix.size();
int col = matrix[0].size();
int result = INT_MIN; //最小的int值
// 行数远大于列数
// left, right为左右边界
for(int left=0; left < col; ++left){
vector<int> sum(row, 0); //用于存储该矩形的行的和
for(int right=left; right < col; ++right){
for(int i=0; i < row; ++i)
sum[i] += matrix[i][right];
result = max(result, helper(row, sum, k));
if(result == k) return result;
}
}
return result;
}
// 用于计算数值和
int helper(const int row, const vector<int> &vec, int k){
int helper_sum=vec[0], max_sum=helper_sum;
for (int i=1; i<row; ++i)
{
if (helper_sum<=0)
helper_sum = vec[i];
else
helper_sum += vec[i];
if (helper_sum > max_sum)
max_sum = helper_sum;
}
if (max_sum <= k)
return max_sum;
max_sum = INT_MIN;
for(int i=0; i < row; ++i){
int sum = 0;
for(int j = i; j < row; ++j){
sum += vec[j];
if(sum == k) return sum;
if(sum < k && sum > max_sum) max_sum = sum;
}
}
return max_sum;
}
};
但是效果挺好的:
还要再接着优化的话,其实在代码中左右边界移动的过程中存在的大量的重复运算,理论上左边界为0的时候,将右边界一直右移到col末尾,这样就已经将整个矩阵遍历了一遍,如果加以记录(典型的空间换时间)的话,后续任意的左右边界之间的矩形产生的vector都是已知的,而不用移动左边界的时候再进行计算,举个例子:
假设矩阵为4列,n行,那么左右边界的所有组合有:
- left = 0: (0,0) (0,1) (0,2) (0,3)
- left = 1: (1,1) (1,2) (1,3)
- left = 2: (2,2) (2,3)
- left = 3: (3,3)
刚好对应着left和right的两层循环,每次循环都对矩阵元素的值进行了累加,但是只有第一次(黄色那一行)是必要的,因为剩下的都可以利用第一行在很少的计算下得到:
(0,0)代表第1行的和,(0,1)代表1,2行的和,(0, 2)代表1,2,3行的和…
(1,1)代表第2行的和,可以由 (0,1) - (0,0)得到,同理:
(1,2)= (0,2) - (0, 0)
(1,3)= (0,3) - (0, 0)
所以第一次遍历的时候,所有的和值其实就已经得到了,如果存储第一次遍历的结果(增加空间),就可以省去后面很多的计算(减少时间)。
其实这就是前缀和的思路嘛!!!
现在就来思考怎么进行存储:
- 首先可以用一个
vector<vector<int>>
来保存第一行的col(列数)个vector<int>
,利用第一行就可以计算出第二,三,四行。
class Solution {
public:
int maxSumSubmatrix(vector<vector<int>>& matrix, int k) {
int row = matrix.size();
int col = matrix[0].size();
int result = INT_MIN; //最小的int值
vector<vector<int>> sum_v;
vector<int> sum(row, 0); //用于存储矩形的行的和,大小为行数
// 此时左边界0,右边界从0到col走了一遍(寻找k的任务也过了一遍)
for(int idx=0; idx < col; ++idx){
for(int i=0; i < row; ++i)
sum[i] += matrix[i][idx];
//此时就可以顺便进行判别,如果找到就节省很多时间,找不到也无所谓。
result = max(result, helper(row, sum, k));
if(result == k) return result;
sum_v.push_back(sum);
}
for(int i = 0; i < sum_v.size()-1; ++i){
for(int j = i+1; j < sum_v.size(); ++j){
for(int p = 0; p != row; ++p){
// 重复利用sum,节省空间
sum[p] = sum_v[j][p] - sum_v[i][p];
}
result = max(result, helper(row, sum, k));
if(result == k) return result;
}
}
return result;
}
// 用于计算数值和
int helper(const int row, const vector<int> &vec, int k){
int helper_sum=vec[0], max_sum=helper_sum;
for (int i=1; i<row; ++i)
{
if (helper_sum<=0)
helper_sum = vec[i];
else
helper_sum += vec[i];
if (helper_sum > max_sum)
max_sum = helper_sum;
}
if (max_sum <= k)
return max_sum;
max_sum = INT_MIN;
for(int i=0; i < row; ++i){
int sum = 0;
for(int j = i; j < row; ++j){
sum += vec[j];
if(sum == k) return sum;
if(sum < k && sum > max_sum) max_sum = sum;
}
}
return max_sum;
}
};
不过代码倒是看起来越来越乱了,非要再优化下去也还是有空间的,利用前缀和相减生成vector, 然后又将vector传入help函数进行遍历的这一步,其实没必要,中间生成的vector就仅仅起到传递的作用,那我将help函数的代码直接挪上来不就省了这一步了嘛?
不过这样会让代码更多更杂。。。