363. 矩形区域不超过 K 的最大数值和
难度:困难
语言:java
题目描述
给你一个 m x n 的矩阵 matrix 和一个整数 k ,找出并返回矩阵内部矩形区域的不超过 k 的最大数值和。
题目数据保证总会存在一个数值和不超过 k 的矩形区域。
解题思路
终于,来了一道困难题了
首先说我自己的思路,第一个想到的是动态规划,dp,如果想要最大,那么只要前面的矩形里面的和尽可能地大,我可以直接通过上一个矩形来判断向下拓展和向右拓展来确定是否需要添加,但是dp有一个问题就是,我没法确定边界,一般dp的情况是,如果加上这个矩形后面积减少,我们就会维持原值,不然就跟做单纯的加减法没有区别了。
我想了一会就去看答案了,答案大法好XD。
答案使用了一个叫做PreSum(前缀和)的方法,这个方法呢,我觉得优化程度也相对一般,无奈也想不出更好的方法了,在此记录一下。
如果我们想求,OD划定矩形的面积,可以用如下公式进行计算,
优化一下,如果我们想求任意矩形的面积可以用如下的公式进行计算,减去两边,加上重复减去的部分
所以这个题目就可以转化为,将这个矩形的所有坐标都求出前缀和,对G这个点要求出OG的面积,对D要求出OD的面积,就可以把一个边界不确定的问题,转化为,从起始点O,到其他所有点所形成的矩形大小的面积,形成一个PreSum(前缀和)矩阵,从而通过加减法就可以求到任意矩形的大小。
为什么我之前说不知道是否够好,就是因为求前缀和的时候需要O(m*n)的复杂度,虽然后面的直接查找PreSum矩阵的复杂度为O(1),但是我还要再找出最大值的方法还是需要优化,我自己想到的还是遍历解决。
更详细的PreSum解释参考
二维前缀和
class Solution {
public int maxSumSubmatrix(int[][] matrix, int k) {
int m = matrix.length , n = matrix[0].length;
int[][] PreSum = new int[m+1][n+1]; // 这里+1是考虑边界的问题
for (int i = 1; i<=m;i++){
for (int j = 1; j<=n;j++){
PreSum[i][j]= PreSum[i-1][j]+PreSum[i][j-1]-PreSum[i-1][j-1]+matrix[i-1][j-1];
}
}
int ans = Integer.MIN_VALUE; //我开始直接用[0][0]带进去的,但是可能初值大于K
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 = PreSum[p][q] - PreSum[i - 1][q] - PreSum[p][j - 1] + PreSum[i - 1][j - 1];
if (cur <= k) {
ans = Math.max(cur,ans);
}
}
}
}
}
return ans;
}
}
这个方法后面就是很麻烦的,四个for嵌套,头都晕了,再看看别的方法吧,下面一个方法呢,前缀和的解法是不变的,但是想到了一个固定边界的方法,简单来说,上面的解法是,四个边同时遍历,因为矩形的大小取决于四个边的位置嘛,这个解法是,只遍历上下两个边界,然后中间的矩形大小中间,选到最大的符合要求的值,那就将一个二维的问题,转换为了一个类似一维的问题,有点像一条直线上截取一段的感觉,一维的解法就是哈希表,这边使用的是树。
注意中间,求子矩形的面积的时候,要让right-left<=k , 就可以转化为right-k<= left,找到最小的left即可,所以使用treeSet的ceiling方法非常合适。
class Solution {
public int maxSumSubmatrix(int[][] mat, int k) {
int m = mat.length, n = mat[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] + mat[i - 1][j - 1];
}
}
int ans = Integer.MIN_VALUE;
// 遍历子矩阵的上边界
for (int top = 1; top <= m; top++) {
// 遍历子矩阵的下边界
for (int bot = top; bot <= m; bot++) {
// 使用「有序集合」维护所有遍历到的右边界
TreeSet<Integer> ts = new TreeSet<>();
ts.add(0);
// 遍历子矩阵的右边界
for (int r = 1; r <= n; r++) {
// 通过前缀和计算 right
int right = sum[bot][r] - sum[top - 1][r];
// 通过二分找 left
Integer left = ts.ceiling(right - k);
if (left != null) {
int cur = right - left;
ans = Math.max(ans, cur);
}
// 将遍历过的 right 加到有序集合
ts.add(right);
}
}
}
return ans;
}
}
就直接借鉴了一下解法了,但是核心思路是这样的,主要要掌握的还是在矩阵中求面积的PreSum(前缀和)方法。