元素和小于等于阈值的正方形的最大边长(来源:力扣(LeetCode))
给你一个大小为 m x n 的矩阵 mat 和一个整数阈值 threshold。
请你返回元素总和小于或等于阈值的正方形区域的最大边长;如果没有这样的正方形区域,则返回 0 。
来源:力扣(LeetCode)
示例1:
输入:mat = [[1,1,3,2,4,3,2],[1,1,3,2,4,3,2],[1,1,3,2,4,3,2]], threshold = 4
输出:2
解释:总和小于或等于 4 的正方形的最大边长为 2,如图所示。
来源:力扣(LeetCode)
示例2:
输入:mat = [[2,2,2,2,2],[2,2,2,2,2],[2,2,2,2,2],[2,2,2,2,2],[2,2,2,2,2]], threshold = 1
输出:0
来源:力扣(LeetCode)
解法一:动态规划
这道题给了一个 m x n 的二维数组和一个整型数 threshold,让返回最大正方形的边长,使得正方形区间内的数字之和小于等于 threshold。像这种求二维数组子区间和的问题,可以很容易联想到一维数组的求子数组之和的问题,通过建立累加和数组可以快速的求出一维数组中任意区间的子数组之和。对于二维数组也是一样的道理,可以建立二维的累加和数组,然后遍历每一个正方形区间,通过累加和数组快速得到其数字之和,然后比较若小于等于 threshold,则用其边长来更新结果 res 即可。二维累加和数组的大小要比原数组大1,这样方便处理越界的问题,累加的方法就是当前位置对应的原数组的数字,加上累加数组上方和左边的数字,减去左上方的数字。
构建完成了累加和数组之后,就可以遍历所有的正方形区间了。由于只需要一个顶点和边长就可以唯一的确定一个正方形区间,所以可以遍历数组中的每一个位置,当作正方形区间的左上顶点,然后遍历所有不越界的边长,并快速求区间和。注意求区间和的方法和求累加和数组的方法是有一些区别的,当正方形区间的左上顶点为 (i, j),边长为 k+1 的时候,则右下顶点为 (i+k, j+k)
,区间和的计算方法是 sums[i + k][j + k] - sums[i - 1][j + k] - sums[i + k][j - 1] + sums[i - 1][j - 1]
,可以自行比较下和计算累加和数组的区别,然后就是和 threshold 比较了,若小于等于 threshold,则用 k+1 来更新结果 res 即可,参见代码如下:
package com.ice.blindweb;
/* @Auther: sugarice
* @Date: 2022/07/24/13:57
* @Description:
*/
public class main {
public static int maxSideLength(int[][] mat, int threshold) {
int m = mat.length, n = mat[0].length;
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
dp[i][j] = mat[i - 1][j - 1] + dp[i - 1][j] + dp[i][j - 1] - dp[i - 1][j - 1];
}
}
int ans = 0;
for (int k = 1; k <= Math.min(m, n); k++) {
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (i - k < 0 || j - k < 0) {
continue;
}
int tmp = dp[i][j] - dp[i - k][j] - dp[i][j - k] + dp[i - k][j - k];
if (tmp <= threshold) {
ans = Math.max(ans, k);
}
}
}
}
return ans;
}
public static void main(String[] args) {
// 示例 1
int[][] mat = {{1, 1, 3, 2, 4, 3, 2}, {1, 1, 3, 2, 4, 3, 2}, {1, 1, 3, 2, 4, 3, 2}};
int i = maxSideLength(mat, 4);
System.out.println(i);
// 示例 2
int[][] mat1 = {{2, 2, 2, 2, 2, 2, 2}, {2, 2, 2, 2, 2, 2, 2}, {2, 2, 2, 2, 2, 2, 2}};
int j = maxSideLength(mat1, 1);
System.out.println(j);
}
}
解法二:动态规划+二分查找
由于所求的最大边长是有范围的,最小为0,最大不超过m和n中的较小值,那么就可以用二分搜索法来增加查找的速度。还是需要建立累加和数组 sums,然后就可以开始二分搜索了,用子函数当作判断关系(通常由 mid 计算得出)。判断的子函数其实就是在整个数组中查找是否存在均有给定边长的正方形区间,使得其数字和小于等于 threshold。因为此时边长确定了,只要遍历左上顶点的位置,然后通过累加和数组快速计算出区间和进行判断即可,参见代码如下:
package com.ice.blindweb;
/* @Auther: sugarice
* @Date: 2022/07/24/14:22
* @Description:
*/
public class main1 {
private static int m, n;
public static int maxSideLength(int[][] mat, int threshold) {
m = mat.length;
n = mat[0].length;
int left = 0;
int right = Math.min(m, n);
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
dp[i][j] = mat[i - 1][j - 1] + dp[i - 1][j] + dp[i][j - 1] - dp[i - 1][j - 1];
}
}
// 二分查找
while (left <= right) {
int mid = left + (right - left) / 2;
if (squareExisted(dp, threshold, mid)) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return right;
}
public static Boolean squareExisted(int[][] sums, int threshold, int len) {
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (i - len < 0 || j - len < 0) {
continue;
}
int tmp = sums[i][j] - sums[i - len][j] - sums[i][j - len] + sums[i - len][j - len];
if (tmp <= threshold) {
return true;
}
}
}
return false;
}
public static void main(String[] args) {
// 示例 1
int[][] mat = {{1, 1, 3, 2, 4, 3, 2}, {1, 1, 3, 2, 4, 3, 2}, {1, 1, 3, 2, 4, 3, 2}};
int i = maxSideLength(mat, 4);
System.out.println(i);
// 示例 2
int[][] mat1 = {{2, 2, 2, 2, 2, 2, 2}, {2, 2, 2, 2, 2, 2, 2}, {2, 2, 2, 2, 2, 2, 2}};
int j = maxSideLength(mat1, 1);
System.out.println(j);
}
}
解法三:一种更加高效的方法
这种方法在建立累加和的过程中就直接进行判断了,而且每次只判断是否有比当前已经存在的正方行边长大1的区间,有的话就让结果 res 自增1,因为左上顶点一次只能移动一个位置,不管是向右,还是向下移动,边长最多也只能增加1。这道题的难点还是在于计算区间时下标的转换,因为此时的正方形区间的右下顶点为 (i, j)
,左上顶点为 (i-res, j-res)
,计算区间和的方法为 sums[i][j] - sums[i - res - 1][j] - sums[i][j - res - 1] + sums[i - res - 1][j - res - 1]
,前提要保证 i - res - 1 和 j - res - 1 均大于等于0
,以防止越界,参见代码如下:
package com.ice.blindweb;
/* @Auther: sugarice
* @Date: 2022/07/24/14:22
* @Description:
*/
public class main2 {
public static int maxSideLength(int[][] mat, int threshold) {
int m = mat.length;
int n = mat[0].length;
int res = 0;
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
dp[i][j] = mat[i - 1][j - 1] + dp[i - 1][j] + dp[i][j - 1] - dp[i - 1][j - 1];
if (i - res - 1 >= 0 && j - res - 1 >= 0 && dp[i][j] - dp[i - res - 1][j] - dp[i][j - res - 1] + dp[i - res - 1][j - res - 1] <= threshold) {
++res;
}
}
}
return res;
}
public static void main(String[] args) {
// 示例 1
int[][] mat = {{1, 1, 3, 2, 4, 3, 2}, {1, 1, 3, 2, 4, 3, 2}, {1, 1, 3, 2, 4, 3, 2}};
int i = maxSideLength(mat, 4);
System.out.println(i);
// 示例 2
int[][] mat1 = {{2, 2, 2, 2, 2, 2, 2}, {2, 2, 2, 2, 2, 2, 2}, {2, 2, 2, 2, 2, 2, 2}};
int j = maxSideLength(mat1, 1);
System.out.println(j);
}
}