【Leetcode】1504. Count Submatrices With All Ones

题目地址:

https://leetcode.com/problems/count-submatrices-with-all-ones/

给定一个 m × n m\times n m×n 0 − 1 0-1 01矩阵 A A A,问其 1 1 1子矩阵的数量。

法1:位运算。本质上是枚举矩阵宽度。对于 A [ i ] [ j ] A[i][j] A[i][j],我们看一下其与其右边最多能延伸出多少个 1 1 1,如果能延伸出 x x x 1 1 1,那么显然长宽都为 1 1 1 1 1 1子矩阵数量就是 1 + 2 + . . . + x = ( 1 + x ) x 2 1+2+...+x=\frac{(1+x)x}{2} 1+2+...+x=2(1+x)x个。如果我们令 A [ i ] [ j ] A[i][j] A[i][j]都按位与一下 A [ i + 1 ] [ j ] A[i+1][j] A[i+1][j],那么此时的 A [ i ] [ j ] A[i][j] A[i][j]就代表了原先矩阵 A [ i : i + 1 ] [ j ] A[i:i+1][j] A[i:i+1][j]是否都是 1 1 1,这样如果改变后的矩阵从 A [ i ] [ j ] A[i][j] A[i][j]能向右延伸出 x x x 1 1 1,就说明以 A [ i ] [ j ] A[i][j] A[i][j]为左上角的宽度为 2 2 2的矩形的最大长度是 x x x,那么宽为 2 2 2 1 1 1子矩阵数量就是 1 + 2 + . . . + x = ( 1 + x ) x 2 1+2+...+x=\frac{(1+x)x}{2} 1+2+...+x=2(1+x)x个。这样就能枚举出以 A [ i ] [ j ] A[i][j] A[i][j]为左上角的所有 1 1 1矩阵的数量了。代码如下:

public class Solution {
    public int numSubmat(int[][] mat) {
        // write your code here
        int res = 0;
        int m = mat.length, n = mat[0].length;
        // 计算宽度为m - bound的以A[i][j]为左上角的1矩阵数量
        for (int bound = m - 1; bound >= 0; bound--) {
            for (int i = 0; i <= bound; i++) {
                int width = 0;
                for (int j = 0; j < n; j++) {
                    if (mat[i][j] == 1) {
                        width++;
                    } else {
                        width = 0;
                    }
                    // 累加1矩阵数量
                    res += width;
                    
                    // 算完了,就向下做按位与
                    if (i < bound) {
                        mat[i][j] &= mat[i + 1][j];
                    }
                }
            }
        }
        
        return res;
    }
}

时间复杂度 O ( m 2 n ) O(m^2n) O(m2n),空间 O ( 1 ) O(1) O(1)

法2:动态规划。设 f [ i ] [ j ] f[i][j] f[i][j]是从 A [ i ] [ j ] A[i][j] A[i][j]开始向右最多能延伸出多少个 1 1 1。则 f [ i ] [ j ] = { 0 , A [ i ] [ j ] = 0 1 + f [ i ] [ j + 1 ] , A [ i ] [ j ] = 1 f[i][j]=\begin{cases} 0,A[i][j]=0\\ 1+f[i][j + 1], A[i][j]=1 \end{cases} f[i][j]={0,A[i][j]=01+f[i][j+1],A[i][j]=1接下来我们开始求以 A [ i ] [ j ] A[i][j] A[i][j]为左上角的 1 1 1矩阵的数量。比如遍历到 ( i , j ) (i,j) (i,j)的时候,以 A [ i ] [ j ] A[i][j] A[i][j]为左上角的上下宽度为 1 1 1 1 1 1矩阵的数量就是 f [ i ] [ j ] f[i][j] f[i][j],而以 A [ i ] [ j ] A[i][j] A[i][j]为左上角的上下宽度为 2 2 2 1 1 1矩阵的数量就是 min ⁡ { f [ i ] [ j ] , f [ i + 1 ] [ j ] } \min\{f[i][j],f[i+1][j]\} min{f[i][j],f[i+1][j]}(类似于法1里的求按位与),接着,以 A [ i ] [ j ] A[i][j] A[i][j]为左上角的上下宽度为 3 3 3 1 1 1矩阵的数量就是 min ⁡ { f [ i ] [ j ] , f [ i + 1 ] [ j ] , f [ i + 2 ] [ j ] } \min\{f[i][j],f[i+1][j],f[i+2][j]\} min{f[i][j],f[i+1][j],f[i+2][j]},等等。这样就能枚举出所有以 A [ i ] [ j ] A[i][j] A[i][j]为左上角的 1 1 1矩阵的数量了,累加起来即可。代码如下:

public class Solution {
    public int numSubmat(int[][] mat) {
        int m = mat.length, n = mat[0].length;
        // 为了节省空间,可以直接把mat当成dp
        for (int j = n - 2; j >= 0; j--) {
            for (int i = 0; i < m; i++) {
                if (mat[i][j] == 1) {
                    mat[i][j] += mat[i][j + 1];
                }
            }
        }
        
        int res = 0;
        for (int j = 0; j < n; j++) {
            for (int i = 0; i < m; i++) {
            	// 存mat[i : k, j]的最小值
                int min = Integer.MAX_VALUE;
                for (int k = i; k < m; k++) {
                    min = Math.min(min, mat[k][j]);
                    // 累加矩阵数量
                    res += min;
                }
            }
        }
        
        return res;
    }
}

时空复杂度与法1同。

法3:单调栈。首先考虑一维的情形,如果给定每个柱子的高度,以数组 h h h表示,考虑以下底边为底的矩形个数。如下图:
在这里插入图片描述
思路是单调栈。维护一个单调上升的栈,当遇到比栈顶低或相等(这里可以低,也可以低或相等,不影响最后答案)的高度时,设栈顶对应的高度是 h t h_t ht,下标为 x t x_t xt,那么栈顶压在下面的高度是其左边第一个比其矮的高度,设为 h i h_i hi,下标为 x i x_i xi,新来的数则是栈顶右边第一个比栈顶低或相等的高度,设为 h j h_j hj,下标为 x j x_j xj,此时,我们计算一下以高度在 [ max ⁡ { h i , h j } + 1 , h t ] [\max\{h_i, h_j\}+1,h_t] [max{hi,hj}+1,ht]区间内,左右延伸是从 x i + 1 x_i+1 xi+1 x j − 1 x_j-1 xj1之间的矩形个数,那么满足上面条件的矩形的个数实际上就等于高度为 1 , 2 , . . . h t − max ⁡ { h i , h j } 1,2,...h_t-\max\{h_i, h_j\} 1,2,...htmax{hi,hj},宽为 x j − x i − 1 x_j-x_i-1 xjxi1的矩形里的矩形个数(原因是,确定了这样的矩形之后,一路把下面的 1 1 1全含进去,就得到了满足上面条件的矩形),而这样的矩形个数是 ( h t − max ⁡ { h i , h j } ) ∗ ( x j − x i − 1 ) ∗ ( x j − x i ) / 2 (h_t-\max\{h_i, h_j\})*(x_j-x_i-1)*(x_j-x_i)/2 (htmax{hi,hj})(xjxi1)(xjxi)/2这个公式的由来是这样的,首先,底边宽度是 w = x j − x i − 1 w=x_j-x_i-1 w=xjxi1,底边肯定要选连续的数,那么选法就是 w + ( w 2 ) = w ( w + 1 ) / 2 w+{w\choose2}=w(w+1)/2 w+(2w)=w(w+1)/2,其次上沿边的选取方案有 h t − max ⁡ { h i , h j } h_t-\max\{h_i, h_j\} htmax{hi,hj}种,所以一共就是 ( h t − max ⁡ { h i , h j } ) ∗ ( x j − x i − 1 ) ∗ ( x j − x i ) / 2 (h_t-\max\{h_i, h_j\})*(x_j-x_i-1)*(x_j-x_i)/2 (htmax{hi,hj})(xjxi1)(xjxi)/2这么多个。如果 x i x_i xi x j x_j xj不存在,那么就想象一下下标为 − 1 -1 1 l h l_h lh的地方有一个高度为 0 0 0的柱子即可。搞定一维的情况之后,考虑二维的情况。首先,我们对每一行计算以该行为底的柱状图每个柱子的高度,然后再按照上面的方式计算以该行为底的矩形个数,最后将所有行的数目累加起来即可。设 f [ i ] [ j ] f[i][j] f[i][j]为以 A [ i ] [ j ] A[i][j] A[i][j]为底的 1 1 1柱子的高度,则 f [ i ] [ j ] = { 0 , A [ i ] [ j ] = 0 1 + f [ i − 1 ] [ j ] , A [ i ] [ j ] = 1 f[i][j]=\begin{cases} 0,A[i][j]=0\\ 1+f[i-1][j], A[i][j]=1 \end{cases} f[i][j]={0,A[i][j]=01+f[i1][j],A[i][j]=1如此就能得出每行为底的柱状图柱子高度了。代码如下:

import java.util.ArrayDeque;
import java.util.Deque;

public class Solution {
    public int numSubmat(int[][] mat) {
        int m = mat.length, n = mat[0].length;
        // 为了节省空间,直接在mat上面操作计算f数组
        for (int i = 1; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (mat[i][j] == 1) {
                    mat[i][j] += mat[i - 1][j];
                }
            }
        }
        
        int res = 0;
        // 枚举行编号,计算以mat[i]为底的矩形数目,累加到res上去
        for (int i = 0; i < m; i++) {
            int count = calculate(mat[i]);
            res += count;
        }
        
        return res;
    }
    
    private int calculate(int[] h) {
        int res = 0;
        Deque<Integer> stack = new ArrayDeque<>();
        // 加入下标-1,想象-1的地方有一个高度为0的柱子
        stack.push(-1);
        for (int i = 0; i < h.length; i++) {
        	// 如果发现了更矮或相等的高度,则违反了单调上升性质,要进行出栈并执行计算
            while (stack.peek() != -1 && h[stack.peek()] >= h[i]) {
                int top = stack.pop();
                
                // 求一下top位置比两边高出来的高度
                int height = 0;
                if (stack.peek() != -1) {
                    height = h[top] - Math.max(h[i], h[stack.peek()]);
                } else {
                    height = h[top] - h[i];
                }
                // 求一下宽度
                int width = i - stack.peek() - 1;
                // 累加矩形数量
                res += height * (width + 1) * width / 2;
            }
            
            stack.push(i);
        }
        
        // 如果栈不空,则说明栈里的柱子右边已经没有比其更矮或者一样的高度了,出栈并执行计算
        while (stack.peek() != -1) {
            int top = stack.pop();
            
            int height = 0;
            if (stack.peek() != -1) {
                height = h[top] - h[stack.peek()];
            } else {
                height = h[top];
            }
            // 此时计算宽度,要想象h.length的地方有一个高度为0的柱子,然后计算
            int width = h.length - stack.peek() - 1;
            // 累加矩形数量
            res += height * (width + 1) * width / 2;
        }
        
        return res;
    }
}

时间复杂度 O ( m n ) O(mn) O(mn),空间 O ( n ) O(n) O(n)

法4:单调栈。还是先递推一下 f [ i ] [ j ] f[i][j] f[i][j],使得 f [ i ] [ j ] f[i][j] f[i][j]是从 ( i , j ) (i,j) (i,j)向上最多有多少个连续的 1 1 1。接着枚举以 A [ i ] [ j ] A[i][j] A[i][j]为右下角的矩形有多少个。我们枚举矩形的底边长度。设 h [ j ] = f [ i ] [ j ] h[j]=f[i][j] h[j]=f[i][j],长度为 1 1 1的时候是 h [ j ] h[j] h[j]个,那么从 h [ j ] h[j] h[j]向左走,走到第一个小于 h [ j ] h[j] h[j]的位置 h [ l ] h[l] h[l]之前,底边从 1 , 2 , . . . 1,2,... 1,2,...开始增长,而最高高度一直都是 h [ j ] h[j] h[j],从而 1 1 1矩阵的数量是 h [ j ] h[j] h[j]乘以下标增长的范围;当走到第一个小于 h [ j ] h[j] h[j]的位置 h [ l ] h[l] h[l]之后,之后统计的 1 1 1矩阵高度不会超过 h [ l ] h[l] h[l],并且个数事实上和以 A [ i ] [ l ] A[i][l] A[i][l]为右下角的 1 1 1矩阵个数是一样多的。所以我们可以用单调上升栈来做,在push的时候找左边第一个小于当前数的位置。代码如下:

import java.util.Deque;
import java.util.LinkedList;

public class Solution {
    public int numSubmat(int[][] mat) {
        for (int i = 1; i < mat.length; i++) {
            for (int j = 0; j < mat[0].length; j++) {
                if (mat[i][j] == 1) {
                    mat[i][j] += mat[i - 1][j];
                }
            }
        }
        
        int res = 0;
        for (int[] row : mat) {
            res += compute(row);
        }
        
        return res;
    }
    
    private int compute(int[] h) {
        int res = 0;
        Deque<int[]> stk = new LinkedList<>();
        for (int i = 0; i < h.length; i++) {
            int s = 0;
            // 找到左边第一个小于h[i]的位置
            while (!stk.isEmpty() && h[stk.peek()[0]] >= h[i]) {
                stk.pop();
            }
            
            // 如果栈不空,则矩阵分为最高高度是h[i]的部分,和最高高度是h[l]的部分;
            // 否则就只有最高高度是h[i]的部分
            if (!stk.isEmpty()) {
                s += (i - stk.peek()[0]) * h[i];
                s += stk.peek()[1];
            } else {
                s += (i + 1) * h[i];
            }
            
            stk.push(new int[]{i, s});
            res += s;
        }
        
        return res;
    }
}

时间复杂度 O ( m n ) O(mn) O(mn),空间 O ( n ) O(n) O(n)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值