363. 矩形区域不超过 K 的最大数值和
思路:给定一个非空二维矩阵 matrix 和一个整数 k,找到这个矩阵内部不大于 k 的最大矩形和。
输入: matrix = [[1,0,1],[0,-2,3]], k = 2
输出: 2
解释: 矩形区域 [[0, 1], [-2, 3]] 的数值和是 2,且 2 是不超过 k 的最大数字(k = 2)。
首先我们需要知道,如何给定任意矩阵计算其内部数之和。
先求出矩阵内任意一点为右下角到整个矩阵左上角这个矩形的面积,这个求法类似动态规划,比较简单:
int m = matrix.length, n = matrix[0].length;
presum = new int[m+1][n+1];
for(int i = 0; i < m; i++)
for(int j = 0; j < n; j++)
presum[i+1][j+1] = presum[i][j+1] + presum[i+1][j] + matrix[i][j] - presum[i][j];
然后可以根据上面算出的结果求给定任意左上角和右下角围成的矩形面积:
private int sumRange(int x1, int y1, int x2, int y2) {
return presum[x2][y2] - presum[x1][y2] - presum[x2][y1] + presum[x1][y1];
}
接下来就进入正题了,如何找到这个矩阵内部不大于 k 的最大矩形和呢?我们需要遍历所有的矩形然后再排序吗,那样时间复杂度会很高,因此我们可以用上TreeSet(),因为treeSet()中有个ceiling()方法可以帮我们解决关键问题。
我们先固定矩阵任意两行,然后使用类似前缀和的思想,顺序遍历每一列,使用TreeSet来找满足k > Sum的最大Sum值,此时的Sum 就等于此位置的前缀和sum减去前面某个位置的前缀和lower,这个lower就需要使用treeSet来寻找,也就是 lower = set.ceiling(sum - k)!!ceiling是求大于某个数的最小数。细节部分见注释:
for(int i = 0; i < n; i++) {//任意两行
for(int j = i + 1; j <= n; j++) {
TreeSet<Integer> set = new TreeSet<>();
set.add(0);//从0开始,必须加入0!!
for(int t = 1; t <= m; t++) {//两行固定,再一列一列遍历(其中一列被固定在第0列,此步骤类似于前缀和的方式,忽略掉行,把每一列当作数组中的某一个元素)
int sum = sumRange(0, i, t, j);
Integer lower = set.ceiling(sum - k);// lower > sum - k
if(lower != null)
ret = Math.max(ret, sum - lower);// k > sum - lower
set.add(sum);
}
}
}
完整代码如下:
private int[][] presum;
public int maxSumSubmatrix(int[][] matrix, int k) {
int m = matrix.length, n = matrix[0].length;
presum = new int[m+1][n+1];
for(int i = 0; i < m; i++)
for(int j = 0; j < n; j++)
presum[i+1][j+1] = presum[i][j+1] + presum[i+1][j] + matrix[i][j] - presum[i][j];
int ret = Integer.MIN_VALUE;
for(int i = 0; i < n; i++) {//任意两行
for(int j = i + 1; j <= n; j++) {
TreeSet<Integer> set = new TreeSet<>();
set.add(0);//从0开始,必须加入0!!
for(int t = 1; t <= m; t++) {//两行固定,再一列一列遍历(其中一列被固定在第0列,此步骤类似于前缀和的方式,忽略掉行,把每一列当作数组中的某一个元素)
int sum = sumRange(0, i, t, j);
Integer lower = set.ceiling(sum - k);// lower > sum - k
if(lower != null)
ret = Math.max(ret, sum - lower);// k > sum - lower
set.add(sum);
}
}
}
return ret;
}
private int sumRange(int x1, int y1, int x2, int y2) {
return presum[x2][y2] - presum[x1][y2] - presum[x2][y1] + presum[x1][y1];
}
还提供一种类似的方法,就是开始没有直接算出矩阵和,而是先求列和再求行和:
public static int maxSumSubmatrix(int[][] matrix, int k) {
int m = matrix.length;
int n = matrix[0].length;
int[][] sums = new int[m][n];
/**
* 按照列来求和
*/
for(int col = 0 ; col < n; col ++){
for(int row = 0 ; row < m ; row ++){
if(row == 0)
sums[row][col] = matrix[row][col];
else
sums[row][col] = sums[row - 1][col] + matrix[row][col];
}
}
/**
* col1为矩阵起始列,col2为矩阵结尾列
*/
int result = Integer.MIN_VALUE ;
for(int col1 = 0 ; col1 < n ; col1 ++){
for(int col2 = col1; col2 < n ; col2++){
/**
* set中都是存放的是startCol和endCol相同的矩阵的和
*/
TreeSet<Integer> set = new TreeSet<>();
set.add(0);
for(int row = 0 ; row < m ; row++){
int sum = 0; //子矩阵的和
for(int i = col1; i <= col2 ; i ++){
sum += sums[row][i];
}
//求出set中大于等于(sum - k)最小值
if(set.ceiling(sum - k) != null){
int max = sum - set.ceiling(sum - k);
result = result > max ? result : max;
}
set.add(sum);
}
}
}
return result;
}