LeetCode 1504

387 阅读4分钟

LeetCode 1504

链接:leetcode.com/problems/co…

方法1:单调栈

时间复杂度:O(mn),m、n分别是矩阵的行列数 想法:我当时没想到,参考的www.acwing.com/solution/co… 。这个思路跟之前的LeetCode 85转化为LeetCode 84有异曲同工之妙。就是说我指定某一行,往上看,看全部是1的矩形的最大高度是多少(这一步可以用一个很简单的dp实现),那就相当于说我有了一个数组,它就像个直方图一样,有一堆宽度为1,长度各异的矩形。之后再用单调栈来解。 如果我已经得到了v数组,它代表这个直方图,每个元素是高度,那么怎么求解全为1的子矩阵个数呢? 我们计算子矩形打算就按照一个一个矩形来计算。这句话听着像是废话但倒也不是,就是说假设有三个矩形,高度分别是3, 4, 2。那么计算这里面有多少个矩形的时候,我将把他们分成4比3高出来那一块,当成一个矩形,3最高的那一块和4的高度为3的那一块,合起来当成一个矩形,然后底下宽度为3,高度为2的矩形再当成另一个整的矩形。对于这里面每个矩形,我们知道长和宽,因此可以用组合数计算有多少个矩形。 为了实现这一点,单调栈每次弹出一个元素的时候,它肯定比弹完之后的栈顶和目前即将入栈的元素大,那么在这个时候,我们就先算它与它的两边的最高值比较,高出来那部分。单调栈放元素下标可以让我们既得到这个高出来的矩形的高度,又能得到宽度。 高度为h,宽度为w的矩形,总共用多少个子矩形?是h * C2w+1. 因为任何一个矩形的高度都在1到h之间,而宽度为w意味着底边是一条长为w的线段,分成长度为1的线段的话会有w条,有w+1个端点,任选两个与h组成矩形。高度只能在1到h之间取,因为当我们用单调栈的时候,每次我们的v数组是以当前的行为底的,我们不去计算那些底不在这条线上的矩形,因为那些矩形已经在之前被计算过了。

代码:

class Solution {
    public int numSubmat(int[][] mat) {
        int m = mat.length, n = mat[0].length;
        int[] v = new int[n + 1];
        int res = 0;
        
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                v[j] = mat[i][j] == 0 ? 0 : v[j] + 1;
            }
            
            Stack<Integer> stack = new Stack<>();
            for (int j = 0; j <= n; j++) {
                while (!stack.isEmpty() && v[stack.peek()] >= v[j]) {
                    int index = stack.pop();
                    int h, w;
                    if (stack.isEmpty()) {
                        h = v[index] - v[j];
                        w = j;
                    }
                    else {
                        h = v[index] - Math.max(v[j], v[stack.peek()]);
                        w = j - stack.peek() - 1;
                    }
                    res += h * (w + 1) * w / 2;
                }
                stack.push(j);
            }
        }
        
        return res;
    }
}

这里的Stack如果用数组实现可以快很多(我写的运行4ms),但对于面试来说的话没什么差别,我就用库函数了。

方法2:DP

时间复杂度:O(m2n),m、n分别是矩阵的行列数 想法:我也没想到,很惭愧。方法来自www.youtube.com/watch?v=Eqz… 大意是开dp[m + 1][n + 1]数组,那么dp[i + 1][j + 1]代表,在原数组中[i, j]这个位置上,它左边总共积累了多少个1了。直接两重for循环,循环到一个点的时候,我可以往上找这一列,维护一个变量代表这一列的值,目前来说,往左数最少有多少个1,然后在res里加上。这个原则上来说是***O(m^2 * n)***的复杂度,但提交上去运行还蛮快的||=_=就很奇怪,但我个人觉得这个题还是掌握第一种做法比较好。

class Solution {
    public int numSubmat(int[][] mat) {
        int m = mat.length, n = mat[0].length;
        int[][] dp = new int[m + 1][n + 1];
        int res = 0;
        
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (mat[i - 1][j - 1] == 1) {
                    dp[i][j] = dp[i][j - 1] + 1;
                    int minOnes = dp[i][j];
                    for (int k = i - 1; k >= 0; k--) {
                        res += minOnes;
                        minOnes = Math.min(minOnes, dp[k][j]);
                    }
                }
            }
        }
        
        return res;
    }
}

我这几天已经复习了一堆题了,但写题解远远赶不上直接复习的进度。目前我没有什么特别好的办法,或许只有当我写出来的时候才完全理解了,这才是到达真正理解所需要花的功夫?我可能会想一些办法来加快写题解的速度,但目前来说还是想继续执行这件事。