LeetCode 最大矩形,最大正方形系列 84. 85. 221. 1277. 1725. (单调栈,动态规划)

84.柱状图中最大的矩形

题目描述

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

示例

输入:heights = [2,1,5,6,2,3]
输出:10

思路

遍历每个柱子,每次算出以当前柱子高度作为矩形的高,往左右两侧扩展,能够得到的最大矩形。再对每个柱子得到的矩形取个最大值即可。

某个柱子往左右两侧扩展,最多能扩展到什么地方呢?自然是遇到第一个比自己更低的柱子,就无法再扩展下去了。于是我们的问题变成了,对于每个柱子,求一下其左侧第一个比它低的柱子,再求一下其右侧第一个比它低的柱子,这两个柱子之间的距离,就是能够扩展的最大的矩形的宽。

于是问题就变成了,在一个数组中,求解每个数左侧第一个比它小的数

对于这种求左侧(右侧)第一个比自己小(大)的问题,都是单调栈的经典应用。

来看下单调栈为什么能够起到作用。假设是求第i个数左侧第一个比自己小的数。

我们从位置i往左走,如果遇到一个数a,这个数比更左侧的所有数都小。那么对于位置i,其答案不可能取到数a更左侧的位置,因为数a左边那些比a大的数,都被a这个数给挡住了。

所以我们能知道一个结论:从右往左走时,先遇到的较小的数,会把后面的更大的数全部挡住,使得更左侧的更大的数不可能作为答案。只有更左侧且更小的数,才可能作为答案。

所以我们从左往右遍历,求解每个数左侧第一个比它小的,只要维护一个单调栈,栈中的数单调递增。

每次求解当前位置答案时,依次弹出栈顶,直到栈顶元素小于当前元素,则此时栈顶就是当前位置的答案,并把当前位置入栈,保持栈内的单调递增。

求解右侧第一个比自己小的数,同理,将遍历顺序反过来即可。

求解第一个比自己大的数也是类似,更早出现的更大的数,会把后面的更小的数全部挡住。不再赘述。

下面给出代码,注意单调栈里存的是下标,因为我们需要计算两根柱子之间的距离,但我们需要按照柱子的高度递增来维护单调栈。

class Solution {
    // 核心思路是, 对于每个柱子, 看其能往左往右扩展多少
    // 需要找到其左侧和右侧第一个比它低的柱子
    public int largestRectangleArea(int[] heights) {
        int n = heights.length;
        int[] stack = new int[n];
        int top = -1;
        int[] left = new int[n]; // 每个位置左侧第一个比自己低的柱子的位置
        int[] right = new int[n]; // 每个位置右侧第一个比自己低的柱子的位置

        for (int i = 0; i < n; i++) {
            // 栈非空, 且栈顶柱子高度比当前柱子高度大, 则弹出栈顶
            while (top >= 0 && heights[stack[top]] >= heights[i]) top--;
            left[i] = top >= 0 ? stack[top] : -1;
            stack[++top] = i;
        }

        top = -1; // clear the stack
        
        for (int i = n - 1; i >= 0; i--) {
            while (top >= 0 && heights[stack[top]] >= heights[i]) top--;
            right[i] = top >= 0 ? stack[top] : n;
            stack[++top] = i;
        }

        int ans = 0;
        for (int i = 0; i < n; i++) {
            ans = Math.max(ans, (right[i] - left[i] - 1) * heights[i]);
        }
        return ans;
    }
}

85.最大矩形

题目描述

给定一个仅包含 01 、大小为 rows x cols 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。

示例

输入:matrix = [[“1”,“0”,“1”,“0”,“0”],[“1”,“0”,“1”,“1”,“1”],[“1”,“1”,“1”,“1”,“1”],[“1”,“0”,“0”,“1”,“0”]]
输出:6
解释:最大矩形如下图所示。

思路

求解的是最大矩形。我们先想一个最直观的做法,那就是枚举所有可能的矩形,并计算每个矩形的面积。怎么来枚举矩形呢?一个矩形由2个点就可以唯一确定,即左上角的点和右下角的点。

我们不妨枚举这个二维矩阵的每个点[x,y],把每个点作为矩形的右下角,尝试找到所有以这个点为右下角的矩形。

那么,只有当这个点为1时,才能至少构成一个矩形。我们尝试找以这个点为右下角的矩形时,一定是要向上向左扩展的。

我们不妨就只向上扩展。而对于向左扩展,我们预处理出每个点左侧连续1的个数这样的信息,不妨设其为left[x][y]

我们称往左扩展的是矩形的宽,往上扩展是矩形的高。

我们是向上扩展的,初始时,矩形的高为1,此时往左扩展最远到left[x][y],此时就能求出以[x,y]为右下角,高为1的矩形的最大面积。再往上扩展,矩形高为2,此时矩形的宽为min(left[x][y], left[x + 1][y]);能够求出高为2的矩形的最大面积;

继续往上扩展,高为3,宽为min(left[x][y], left[x + 1][y], left[x + 2][y])。后面同理。

直到无法往上扩展为止。

根据这个思路,写出代码如下:

class Solution {
    public int maximalRectangle(char[][] matrix) {
        int n = matrix.length, m = matrix[0].length;
        int[][] left = new int[n][m]; // 每个点左侧连续1的个数
        for (int i = 0; i < n; i++) {
            int len = 0;
            for (int j = 0; j < m; j++) {
                if (matrix[i][j] == '1') len++;
                else len = 0;
                left[i][j] = len;
            }
        }

        int ans = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (matrix[i][j] == '0') continue;
                int width = left[i][j];
                for (int k = i; k >= 0; k--) {
                    if (matrix[k][j] == '0') break; // 无法向上扩展了
                    width = Math.min(width, left[k][j]); // 更新宽度
                    ans = Math.max(ans, width * (i - k + 1)); // 计算矩形面积
                }
            }
        }
        return ans;
    }
}

预处理左侧连续1的个数,时间复杂度 O ( n × m ) O(n×m) O(n×m),对于每个点作为矩形右下角,向上扩展找到全部矩形,时间复杂度 O ( n × m × n ) O(n×m×n) O(n×m×n),总时间复杂度为 O ( n × n × m ) O(n×n×m) O(n×n×m)

优化

根据上面的思路,我们容易发现,由于我们对每个点是往上扩展。我们可以将每一列拿出来单独看,由于每个点预处理了其左侧连续1的个数。对于某一列,这一列上每个点左侧连续1的个数,就可以看作柱子的高度,只不过这些柱子是以当前列为基线,往左侧生长的。

所以,我们单独看每一列时,其实就是求解第84题了。于是可以套用84题单调栈的解法。对于每一列,求解的复杂度为 O ( n ) O(n) O(n),一共有m列,所以总复杂度为 O ( n × m ) O(n×m) O(n×m)

class Solution {
    public int maximalRectangle(char[][] matrix) {
        int n = matrix.length, m = matrix[0].length;
        int[][] left = new int[n][m]; // 左侧连续1的个数
        for (int i = 0; i < n; i++) {
            int len = 0;
            for (int j = 0; j < m; j++) {
                if (matrix[i][j] == '1') len++;
                else len = 0;
                left[i][j] = len;
            }
        }
        int[] stack = new int[n];
        int top = -1;
        int ans = 0;
        int[] down = new int[n]; // 每一列下侧第一个比当前位置小的
        int[] up = new int[n]; // 每一列上侧第一个比当前位置小的
        for (int i = 0; i < m; i++) {
            top = -1; // clear stack
            for (int j = n - 1; j >= 0; j--) {
                while (top >= 0 && left[j][i] <= left[stack[top]][i]) top--;
                down[j] = top >= 0 ? stack[top] : n;
                stack[++top] = j;
            }
            top = -1; // clear stack
            for (int j = 0; j < n; j++) {
                while (top >= 0 && left[j][i] <= left[stack[top]][i]) top--;
                up[j] = top >= 0 ? stack[top] : -1;
                stack[++top] = j;
            }

            for (int j = 0; j < n; j++) {
                ans = Math.max(ans, (down[j] - up[j] - 1) * left[j][i]);
            }
        }
        return ans;
    }
}

221.最大正方形

上面求解的是最大矩形,现在来看一下一道类似的题目,最大正方形

题目描述

在一个由 '0''1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,并返回其面积。

示例

输入:matrix = [[“1”,“0”,“1”,“0”,“0”],[“1”,“0”,“1”,“1”,“1”],[“1”,“1”,“1”,“1”,“1”],[“1”,“0”,“0”,“1”,“0”]]
输出:4

思路

同样的,我们考虑一个正方形可以怎么表示。和矩形用左上和右下两个顶点表示不同,我们可以只用一个顶点,加上边长,来表示一个正方形。所以我们考虑,枚举二维矩阵中的每个点,找到以这个点为右下角的最大的正方形。

与上面求最大矩形不同。这道题用DP来做。

我们设f(i,j)来表示,以[i,j]为右下角的最大正方形的边长。

我们以[i,j]为右下角,找最大正方形,则要看其往上和往左,能延申的最长长度。观察可以发现, 以[i,j]为右下角的最大正方形的边长,会受制于以[i - 1, j],以[i, j - 1],以[i - 1, j - 1]为右下角的最大正方形的边长。于是得到状态转移方程

f[i][j] = min(f[i - 1][j], f[i][j - 1], f[i - 1][j - 1]) + 1

我们来证明一下这个状态转移方程的正确性。

假设f[i][j] = k,则f[i - 1][j]至少为k - 1f[i][j - 1]也至少为k - 1f[i - 1][j - 1]也至少为k - 1,于是容易得出

min(f[i - 1][j], f[i][j - 1], f[i - 1][j - 1]) >= k - 1

min(f[i - 1][j], f[i][j - 1], f[i - 1][j - 1]) + 1 >= f[i][j]

接下来只要证一下>关系是不成立的即可。

min(f[i - 1][j], f[i][j - 1], f[i - 1][j - 1]) + 1 > f[i][j]

f[i - 1][j] > f[i][j] - 1

f[i][j - 1] > f[i][j] - 1

f[i - 1][j - 1] > f[i][j] - 1

f[i - 1][j] >= f[i][j]

f[i][j - 1] >= f[i][j]

f[i - 1][j - 1] >= f[i][j]

画个图可以得到,当这个条件成立时,f[i][j]能够取到的最大值至少是k + 1,而不是k,这就与先前的假设矛盾了。所以>关系一定不成立,进而有f[i][j] = min(f[i - 1][j], f[i][j - 1], f[i - 1][j - 1]) + 1

class Solution {
    public int maximalSquare(char[][] matrix) {
        int n = matrix.length, m = matrix[0].length;
        int[][] f = new int[n + 1][m + 1];
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                if (matrix[i - 1][j - 1] == '1') {
                    // 三者取最小
                    f[i][j] = Math.min(f[i - 1][j], f[i][j - 1]);
                    f[i][j] = Math.min(f[i][j], f[i - 1][j - 1]);
                    // 加一
                    f[i][j]++;
                    ans = Math.max(ans, f[i][j]);
                }
            }
        }
        return ans * ans; // 要求的是面积
    }
}

由于每个状态只依赖于左,上,左上,这三个状态,可以用滚动数组进行空间上的优化

class Solution {
    public int maximalSquare(char[][] matrix) {
        int n = matrix.length, m = matrix[0].length;
        int[] f = new int[m + 1];
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            int topLeft = 0; // 左上角
            for (int j = 1; j <= m; j++) {
                if (matrix[i - 1][j - 1] == '1') {
                    // 三者取最小
                    int temp = f[j]; // 暂存, 作为下一轮迭代的左上角
                    f[j] = Math.min(f[j], f[j - 1]);
                    f[j] = Math.min(f[j], topLeft);
                    // 加一
                    f[j]++;
                    ans = Math.max(ans, f[j]);
                    topLeft = temp;
                } else {
                    f[j] = 0; //如果当前位置不是1, 则要更新为0
                }
            }
        }
        return ans * ans; // 要求的是面积
    }
}

1277.统计全为1的正方形子矩阵

题目描述

给你一个 m * n 的矩阵,矩阵中的元素不是 0 就是 1,请你统计并返回其中完全由 1 组成的 正方形 子矩阵的个数。

思路

和221的思路一样,221我们用f[i][j]维护了最大正方形的边长,而以[i,j]为右下角的正方形的个数,就等于最大正方形的边长。

最终的答案把所有的f[i][j]做一下累加即可

class Solution {
    public int countSquares(int[][] matrix) {
        int n = matrix.length, m = matrix[0].length;
        int[][] f = new int[n + 1][m + 1];
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                if (matrix[i - 1][j - 1] == 1) {
                    f[i][j] = Math.min(f[i - 1][j], f[i][j - 1]);
                    f[i][j] = Math.min(f[i][j], f[i - 1][j - 1]) + 1;
                    ans += f[i][j];
                }
            }
        }
        return ans;
    }
}

1725.可以形成最大正方形的矩阵数目

题目描述

给你一个数组 rectangles ,其中 rectangles[i] = [li, wi] 表示第 i 个矩形的长度为 li 、宽度为 wi 。

如果存在 k 同时满足 k <= li 和 k <= wi ,就可以将第 i 个矩形切成边长为 k 的正方形。例如,矩形 [4,6] 可以切成边长最大为 4 的正方形。

设 maxLen 为可以从矩形数组 rectangles 切分得到的 最大正方形 的边长。

请你统计有多少个矩形能够切出边长为 maxLen 的正方形,并返回矩形 数目 。

思路

维护最大正方形的边长,并进行累加即可。当出现了更大的正方形,则将累加的数目重置。

class Solution {
    public int countGoodRectangles(int[][] rectangles) {
        int maxLen = 0, cnt = 0;
        for (int[] r : rectangles) {
            // 该矩形能切出的最大正方形
            int len = Math.min(r[0], r[1]);
            if (maxLen == len) cnt++; // 如果等于最大正方形, 则累加
            else if (maxLen < len) {
                // 否则, 出现了更大的正方形, 计数重置
                maxLen = len;
                cnt = 1;
            }
        }
        return cnt;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值