最大子矩阵和
前言
预处理–前缀和,为解题赋能。计算区间由O(n) ->O(1);双指针滑动窗口+变量记录想要的解。
一、最大子矩阵和
二、二维前缀和+双指针
package com.xhu.offer.everyday;
import java.beans.beancontext.BeanContext;
//最大子矩阵
public class GetMaxMatrix {
/*
target:找所有子矩阵中最小的子矩阵和的左上行列坐标和右下行列坐标。
如何找子矩阵?选两列选两行,围成一个子矩阵。m行n列,所以有m * m种行的取法,n * n种列的取法。即O(m*m*n*n)时间复杂度。
如何得到子矩阵的左上行列和右下行列?((第一行,第一列),(第二行,第二列))。
如何得到最小的子矩阵?
1-矩阵直接求和,但缺点太明显。
2-二维前缀和来O(1)求得子矩阵和。
如何求二维前缀和?从第一行开始遍历,当前位置的二维前缀和 = 上面的前缀和 + 左边的前缀和 - 重复计算左上块和 + 自己。
如何利用前缀和来求子矩阵的和?sum = 右下前缀和 - (左下列-1的前缀和)-(右上行-1的前缀和) + (多减去的左上行列减一的前缀和)
M1:Pre-二维前缀和 + 暴力取子矩阵。
*/
public int[] getMaxMatrix(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
//1-预处理前缀和
int[][] prefix = getPrefixSum(matrix, m, n);
//2-暴力求解
int[] rs = {0, 0, 0, 0};
int max = 1 << 31;
for (int i = 0; i < m; i++) {
for (int j = i; j < m; j++) {
for (int k = 0; k < n; k++) {
for (int l = k; l < n; l++) {
int v1 = prefix[j][l];
int v2 = k == 0 ? 0 : prefix[j][k - 1];
int v3 = i == 0 ? 0 : prefix[i - 1][l];
int v4 = i == 0 || k == 0 ? 0 : prefix[i - 1][k - 1];
int sum = v1 - v2 - v3 + v4;
if (sum > max) {
max = sum;
rs[0] = i;
rs[1] = k;
rs[2] = j;
rs[3] = l;
}
}
}
}
}
//返回行列坐标
return rs;
}
private int[][] getPrefixSum(int[][] matrix, int m, int n) {
int[][] prefix = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int v1 = i == 0 ? 0 : prefix[i - 1][j];//上面前缀和。
int v2 = j == 0 ? 0 : prefix[i][j - 1];//左面前缀和。
int v3 = i == 0 || j == 0 ? 0 : prefix[i - 1][j - 1];//上面和左面的公共部分。
//计算当前位置前缀和
prefix[i][j] = v1 + v2 - v3 + matrix[i][j];
}
}
return prefix;
}
}
//方法一超时
class GetMaxMatrix2 {
/*
target:找所有子矩阵中最小的子矩阵和的左上行列坐标和右下行列坐标。
如何找子矩阵?选两列选两行,围成一个子矩阵。m行n列,所以有m * m种行的取法,n * n种列的取法。即O(m*m*n*n)时间复杂度。
如何得到子矩阵的左上行列和右下行列?((第一行,第一列),(第二行,第二列))。
如何得到最小的子矩阵?
1-矩阵直接求和,但缺点太明显。
2-二维前缀和来O(1)求得子矩阵和。
如何求二维前缀和?从第一行开始遍历,当前位置的二维前缀和 = 上面的前缀和 + 左边的前缀和 - 重复计算左上块和 + 自己。
如何利用前缀和来求子矩阵的和?sum = 右下前缀和 - (左下列-1的前缀和)-(右上行-1的前缀和) + (多减去的左上行列减一的前缀和)
M1:Pre-二维前缀和 + 暴力取子矩阵。
M1:超时。
可否改进?题目还有条件:有负数的存在,所以可去掉无意义的前缀部分即sum<=0的前缀部分。
所以当行固定时,可确定第一列的位置,使得不加上负数前缀和。代码体现为sum<=0时,不要左边的前缀部分即重设第一列,来将复杂度降到O(m*m*n)
*/
public int[] getMaxMatrix(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
//1-预处理前缀和
int[][] prefix = getPrefixSum(matrix, m, n);
//2-暴力求解
int[] rs = {0, 0, 0, 0};
int max = 1 << 31;
for (int i = 0; i < m; i++) {
for (int j = i; j < m; j++) {
int k = 0;
for (int l = 0; l < n; l++) {
int v1 = prefix[j][l];
int v2 = k == 0 ? 0 : prefix[j][k - 1];
int v3 = i == 0 ? 0 : prefix[i - 1][l];
int v4 = i == 0 || k == 0 ? 0 : prefix[i - 1][k - 1];
int sum = v1 - v2 - v3 + v4;
if (sum > max) {
max = sum;
rs[0] = i;
rs[1] = k;
rs[2] = j;
rs[3] = l;
}
if (sum <= 0) {
k = l + 1;
}
}
}
}
//返回行列坐标
return rs;
}
private int[][] getPrefixSum(int[][] matrix, int m, int n) {
int[][] prefix = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
int v1 = i == 0 ? 0 : prefix[i - 1][j];//上面前缀和。
int v2 = j == 0 ? 0 : prefix[i][j - 1];//左面前缀和。
int v3 = i == 0 || j == 0 ? 0 : prefix[i - 1][j - 1];//上面和左面的公共部分。
//计算当前位置前缀和
prefix[i][j] = v1 + v2 - v3 + matrix[i][j];
}
}
return prefix;
}
//[[9,-8,1,3,-2],[-3,7,6,-2,4],[6,-4,-4,8,-7]]
}
总结
1)常见题型:前缀和,这里作为里预处理组件,配合其它component来解题,所以是个困难题,毕竟要求单个知识的扎实掌握和多个扎实知识的逻辑组合性、连贯性。
2)常见的双指针,双指针作用很多,核心就是通过改变左右窗口端来使数据遍历两遍,从而将时间复杂度降到O(n)。
注:1-双指针一般配合max或min变量记录多个窗口中想要的窗口及窗口转化的结果;2-保存连续子数组问题对双指针的敏感性。
参考文献
[1] LeetCode 最大子矩阵和