题目描述 :给定一个正整数、负整数和 0 组成的 N × M 矩阵,编写代码找出元素总和最大的子矩阵。
    返回一个数组 [r1, c1, r2, c2],其中 r1, c1 分别代表子矩阵左上角的行号和列号,r2, c2 分别代表右下角的行号和列号。若有多个满足条件的子矩阵,返回任意一个均可。

    示例1

    输入:
    [
    [-1,0],
    [0,-1]
    ]
    输出:[0,1,0,1]
    解释:输入中标粗的元素即为输出所表示的矩阵

    思考 : 动态规划问题
    1、最最最暴力的解法

    • 遍历二维数组的每一个点,(r1, c1),尝试构建一个子矩阵[r1,c1] ~ [r2,c2](子矩阵左上角到右下角坐标),然后计算这个子矩阵。
    • 这一过程的时间复杂度为:O(N6)。遍历二维矩阵需要O(N2)的时间复杂度,构造子矩阵需要O(N2)的时间复杂度,计算子矩阵需要O(N2)的时间复杂度。
    • 空间复杂度:O(N2),可以创建一个二维数组,重复使用。
    • 这个解法没有任何可行性,我们考虑进行一定的优化。

    2、暴力解法优化一(不单单是这道题,针对二维子矩阵和的问题均可用这样暴力解法,然后再根据题目进一步考虑优化问题)

    • 遍历二维数组的每一个点,(r1, c1),尝试构建一个子矩阵[r1,c1] ~ [r2,c2](子矩阵左上角到右下角坐标),然后计算这个子矩阵。
    • 针对第1点的解法,我们尝试优化计算子矩阵的时间复杂度,将其降为O(1)
    • 我们尝试构建一个四维的dp数组来完成上述的想法,dp[i1][j1][i2][j2]代表子矩阵左上角坐标为(i1, j1),右下角坐标为(i2, j2)的最大和。
    • 状态转移方程(此处利用了容斥原理):
    • dp[i1][j1][i2][j2] = dp[i1][j1][i2-1][j1] + dp[i1][j1][i2][j2-1] - dp[i1][j1][i2-1][j2-1] + matrix[i2][j2]。
    • 这一过程的时间复杂度为:O(N4)。遍历二维矩阵需要O(N2)的时间复杂度,构造子矩阵需要O(N2)的时间复杂度,计算子矩阵需要O(1)的时间复杂度。
    • 空间复杂度为:O(N4),空间复杂度太高了,很容易就爆内存了。空间换时间

    代码如下(不能AC,14/45):

    1. class Solution {
    2. public int[] getMaxMatrix(int[][] matrix) {
    3. int R = matrix.length;
    4. int C = matrix[0].length;
    5. int[][][][] dp = new int[R+1][C+1][R+1][C+1];
    6. int maxSum = Integer.MIN_VALUE;
    7. int[] ans = new int[4];
    8. int r1 = 0, c1 = 0;
    9. for(int i1 = 1; i1 <= R; i1++){
    10. for(int j1 = 1; j1<= C; j1++){
    11. r1 = i1;
    12. c1 = j1;
    13. dp[i1][j1][i1][j1] = matrix[i1-1][j1-1];
    14. for(int i2 = i1; i2 <= R; i2++){
    15. for(int j2 = j1; j2<= C; j2++){
    16. dp[i1][j1][i2][j2] = dp[i1][j1][i2][j2-1] + dp[i1][j1][i2-1][j2] -
    17. dp[i1][j1][i2-1][j2-1] + matrix[i2-1][j2-1];
    18. if(dp[i1][j1][i2][j2] > maxSum){
    19. maxSum = dp[i1][j1][i2][j2];
    20. ans[0] = r1-1;
    21. ans[1] = c1-1;
    22. ans[2] = i2-1;
    23. ans[3] = j2-1;
    24. }
    25. }
    26. }
    27. }
    28. }
    29. return ans;
    30. }
    31. }

    3、暴力解法优化二

    • 由于第 2 点的解法空间复杂度太高,在此尝试进一步优化。
    • 我们构造子矩阵都为由 (i1, j1) 开始的,由此设计的状态方程 dp[i1][j1][i2][j2],但是,每当(i1, j1)发生变化时,dp[i1][j1][][] 与 dp[new_i1][new_j1][][] 之间并无状态转移的联系,所以,我们把状态方程删去前两维,(i1, j1)状态变成隐式了(在前两层 for 循环体现,而不再状态方程中体现),新的状态方程为dp[i2][j2],在外面两层循环的遍历中,对每一个 (i1, j1) 而言,dp[i2][j2] 仍然可以表示 子矩阵

    (左上角为(i1, j1),右下角为(i2, j2))的最大和,只是空间层面优化了两维。

    • 与第 2 点的时间复杂度一样,空间复杂度优化为O(N2)。

    代码如下(不能AC,42/45)

    class Solution {
        public int[] getMaxMatrix(int[][] matrix) {
            int R = matrix.length;
            int C = matrix[0].length;
            int[][] dp = new int[R+1][C+1];
            int maxSum = Integer.MIN_VALUE;
            int[] ans = new int[4];
            int r1 = 0, c1 = 0;
            for(int i1 = 1; i1 <= R; i1++){
                for(int j1 = 1; j1<= C; j1++){
                    for(int[] OneDimension : dp){//将二维dp置0
                        Arrays.fill(OneDimension, 0);
                    }
                    r1 = i1;
                    c1 = j1;
                    dp[i1][j1] = matrix[i1-1][j1-1];
                    for(int i2 = i1; i2 <= R; i2++){
                        for(int j2 = j1; j2<= C; j2++){
                            dp[i2][j2] = dp[i2][j2-1] + dp[i2-1][j2] -
                                        dp[i2-1][j2-1] + matrix[i2-1][j2-1];
                            if(dp[i2][j2] > maxSum){
                                maxSum = dp[i2][j2];
                                ans[0] = r1-1;
                                ans[1] = c1-1;
                                ans[2] = i2-1;
                                ans[3] = j2-1;
                            }
                        }
                    }
                }
            }
            return ans;
        }
    }
    

    4、状态压缩(二维DP压缩成一维DP)

    • 由上述暴力解法可以得知,时间复杂度为O(N4),不能 AC 这道题。接下来,我们尝试对时间复杂度进行优化。
    • 总结上述暴力解法,可以得出,我们求解子矩阵最大和需要遍历 [i1, j1, i2, j2] 这四个变量,i 和 j 对应两个维度,有这么一种思想,固定住一维,然后关注另一维的变化情况。比如我们可以固定 i1 和 i2,在 i1 和 i2 固定的前提下,我们从 j1 遍历到 j2,这样就可以计算出 在 第 i1 行 到 第 i2 行范围内的最大子矩阵之和(还需要利用一维前缀和算法),然后我们再对 i1 和 i2 做遍历,就能以 O(NNM) 的时间复杂度解决该问题。
    • 对于这个解法,还有很多不理解的小细节,比如为什么可以将二维上的前缀和压缩成一维处理,目前没有一个直观的认识,等待继续学习
    • 时间复杂度:O(NNM),其中 N 为矩阵行数,M 为矩阵列数。
    • 空间复杂度:O(M)。

    代码如下(AC):

    class Solution {
        public int[] getMaxMatrix(int[][] matrix) {
            int R = matrix.length;
            int C = matrix[0].length;
            int[] ans = new int[4];
            int row = 0, col = 0; //最大子矩阵左上角坐标
            int[] dp = new int[C];
            int maxSubMatrixSum = Integer.MIN_VALUE;
            for(int r1 = 0; r1 < R; r1++){//枚举行维度的上边界
                Arrays.fill(dp, 0);//每更新上边界,dp也需要更新
                for(int r2 = r1; r2 < R; r2++){//枚举行维度的下边界
                    int sum = 0;
                    for(int c = 0; c < C; c++){//遍历左右边界(列维度)
                        //进行一维的前缀和问题求解
                        dp[c] += matrix[r2][c];
                        if(sum > 0){
                            //一维dp中有 dp[i] = max{dp[i-1]+nums[i], nums[i]}
                            //因此,上式中的赋值取决于dp[i-1] > 0 
                            //在此处,Sum就等于 dp[i-1]
                            sum += dp[c];
                        }else{
                            //放弃之前的前缀和,从新计算
                            //顺带维护最大子矩阵左上角坐标
                            sum = dp[c];
                            row = r1; 
                            col = c;
                        }
                        if(sum > maxSubMatrixSum){
                            maxSubMatrixSum = sum;
                            ans[0] = row;
                            ans[1] = col;
                            //ans[2]~ans[3]为最大子矩阵右下角坐标
                            ans[2] = r2;
                            ans[3] = c;
                        }
                    }
                }
            }
            return ans;
        }
    }