动态规划和单调栈:矩阵和正方形系列

LeetCode84. 柱状图中最大的矩形

https://leetcode-cn.com/problems/largest-rectangle-in-histogram/

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

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

在这里插入图片描述

图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10 个单位。

输入: [2,1,5,6,2,3]
输出: 10
思路(暴力)

依次遍历柱形的高度,对于每一个高度分别向两边扩散,求出以当前高度为矩形的最大宽度多少。

为此,我们需要:

  • 左边看一下,看最多能向左延伸多长,找到大于等于当前柱形高度的最左边元素的下标;

  • 右边看一下,看最多能向右延伸多长;找到大于等于当前柱形高度的最右边元素的下标。

对于每一个位置,我们都这样操作,得到一个矩形面积,求出它们的最大值。

代码
class Solution 
{
public:
    //暴力
    int largestRectangleArea(vector<int>& heights) 
    {
        int sz = heights.size();
        if (sz == 0) return 0;

        int maxArea = 0;
        for (int i = 0; i < sz; ++i)
        {
            //向左
            int left;
            for (left = i; left >=0 && heights[left] >= heights[i]; --left);

            //向右
            int right;
            for (right = i; right < sz && heights[right] >= heights[i]; ++right);

            int width = right - left - 1;
            maxArea = max(maxArea, heights[i] * width);
        }

        return maxArea;
    }
};
复杂度分析
  • 时间复杂度:O(N^2),LeetCode上无法通过所有case
  • 空间复杂度:O(1)
思路(单调栈优化)
  1. 单调栈分为单调递增栈和单调递减栈

    • 单调递增栈即栈内元素保持单调递增的栈
    • 单调递减栈即栈内元素保持单调递减的栈
  2. 操作规则(下面都以单调递增栈为例)

    • 如果新的元素比栈顶元素大,就入栈
    • 如果新的元素较小,那就一直把栈内元素弹出来,直到栈顶比新元素小
  3. 性质

    • 栈内的元素是递增的

    • 当元素出栈时,说明这个新元素是出栈元素向后找第一个比其小的元素

      举个例子栈里是 1 5 6 。
      接下来新元素是 2 ,那么 6 需要出栈。
      当 6 出栈时,右边 2 代表是 6 右边第一个比 6 小的元素。

    • 当元素出栈后,说明新栈顶元素是出栈元素向前找第一个比其小的元素

      当 6 出栈时,5 成为新的栈顶,那么 5 就是 6 左边第一个比 6 小的元素。

  4. 代码模板

stack<int> st;
for(int i = 0; i < nums.size(); ++i)
{
	while(!st.empty() && nums[i] < st.top())
	{
		st.pop();
	}
	st.push(nums[i]);
}

  1. 图示

在这里插入图片描述

有了单调栈的知识那么我们就可以知道,当单调栈有元素出栈时,新元素一定是出栈元素右边第一个比自己小的元素,而新栈顶元素一定是出战元素左边第一个比自己小的元素,这样我们就可以得到对应高度(即出栈元素)矩形的最大宽度,就能计算出矩形面积。

注意需要考虑两种特殊的情况:

  • 弹栈的时候,栈为空;

  • 遍历完成以后,栈中还有元素;

为此可以我们可以在输入数组的两端加上两个高度为 0 的柱形(哨兵)

有了这两个柱形:

  • 最左边的柱形:由于它一定比输入数组里任何一个元素小,它肯定不会出栈,因此栈一定不会为空;

  • 最右边的柱形:因为它一定比输入数组里任何一个元素小,它会让所有输入数组里的元素出栈(第 1 个哨兵元素除外)。

代码
class Solution 
{
public:
    //单调栈优化
    int largestRectangleArea(vector<int>& heights)
    {
        heights.insert(heights.begin(), 0);
        heights.push_back(0);
        int sz = heights.size();

        int maxArea = 0;
        stack<int> stk;
        for (int i = 0; i < sz; ++i)
        {
            while (!stk.empty() && heights[i] < heights[stk.top()])
            {
                int height = heights[stk.top()];
                stk.pop();

                int right = i;
                int left = stk.top();
                int width = right - left - 1;

                maxArea = max(maxArea, height * width);
            }

            stk.push(i);
        }

        return maxArea;
    }
};
复杂度分析
  • 时间复杂度:O(N)
  • 空间复杂度:O(N)

LeetCode85. 最大矩形(LeetCode84. 柱状图中最大的矩形进阶)

https://leetcode-cn.com/problems/maximal-rectangle/

给定一个仅包含 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
解释:最大矩形如上图所示。
思路

使用数组dp动态记录每一行中对应列的连续1长度,将这些连续1长度当作柱状图中柱子高度。即可转换为LeetCode84. 柱状图中最大的矩形(见上文)。
例如:
matrix:
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
动态(或者说滑动)记录的dp数组
1 0 1 0 0
2 0 2 1 1
3 1 3 2 2
4 0 0 3 0
每一行都可作为一个柱状图,转换为去计算柱状图中最大的矩形,最后找出最大面积。

代码
class Solution 
{
private:
    int largestRectangleArea(const vector<int>& heights) 
    {
        //不能改变原数组,所以将数组dp中的元素都复制到双向队列中
        deque<int> copyHeights(heights.begin(), heights.end());
        copyHeights.push_front(0);
        copyHeights.push_back(0);

        int maxArea = 0;
        stack<int> stk;
        for (int i = 0;i < copyHeights.size();++i)
        {
            while(stk.empty() == false && copyHeights[i] < copyHeights[stk.top()])
            {
                int height = copyHeights[stk.top()];
                stk.pop();

                int right = i;
                int left = stk.top();
                int width = right - left - 1;

                maxArea = max(maxArea, height * width);
            }

            stk.push(i);
        }

        return maxArea;      
    }

public:
    int maximalRectangle(vector<vector<char>>& matrix) 
    {
        if (matrix.size() == 0) return 0;

        int rows = matrix.size();
        int cols = matrix[0].size();

        vector<int> dp(cols, 0);

        int result = 0;
        for (int i = 0;i < rows;++i)
        {
            for (int j = 0;j < cols;++j)
            {
                /*使用数组dp动态记录每一行中对应列的连续1长度,将这些连续1长度当
                 作柱状图中柱子高度。即可转换为LeetCode84. 柱状图中最大的矩形*/
                dp[j] = matrix[i][j] == '1' ? dp[j] + 1 : 0;
            }
            result = max(result, largestRectangleArea(dp));
        }

        return result;
    }
};

LeetCode42. 接雨水

https://leetcode-cn.com/problems/trapping-rain-water/

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

在这里插入图片描述

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 
思路(暴力)

根据题意可得:只有当前柱左右两边都有比自己高的柱子才能接到雨水,

所以找当前柱左右两边最高的柱子,用两者的较低值减去当前柱。

代码
class Solution 
{
public:
    //只有当前柱左右两边都有比自己高的柱子才能接到雨水
    //找当前柱左右两边最高的柱子,用两者的较低值减去当前柱
    int trap(vector<int>& height)
    {
        int sz = height.size();
        if (sz == 0) return 0;

        int result = 0;
        for (int i = 1;i < sz - 1; ++i)
        {
            int left, maxLeft = height[i];
            for (left = i - 1; left >=0; --left)
            {
                maxLeft = max(maxLeft, height[left]);
            }

            int right, maxRight = height[i];
            for (right = i + 1; right < sz; ++right)
            {
                maxRight = max(maxRight, height[right]);
            }

            result += min(maxLeft, maxRight) - height[i];
        }

        return result;
    }
}
复杂度分析
  • 时间复杂度:O(N ^ 2)
  • 空间复杂度:O(1)
思路(动态规划)

在暴力解中,对于每一列,我们求它左边最高的墙和右边最高的墙,都是重新遍历一遍所有高度,这里我们可以使用动态规划的思想优化一下。

1.思考状态:找出最优解的性质,明确状态表示什么

maxLeft[i]表示当前位置i的左边历史最高柱(包括i)

maxRight[i]表示当前位置i的右边历史最高柱(包括i)

2.思考状态转移方程:大问题的最有解如何由小问题的最优解得到

maxLeft[i]的值有两个来源:

  • 一个是maxLeft[i - 1],代表位置i - 1的左边历史最高柱(包括i - 1)
  • 一个是height[i],代表位置i柱的高度

显然应取两者的较大值。

所以:maxLeft[i] = max(maxLeft[i - 1], height[i])

maxRight[i]同理

所以:maxRight[i] = max(maxRight[i + 1], height[i])

3.思考初始状态

maxLeft[0] = height[0]

maxRight[sz - 1] = height[sz - 1]

4.自底向上计算得到最优解

        for (int i = 1;i < size;++i)
        {
            maxLeft[i] = max(height[i], maxLeft[i - 1]);
        }
        for (int i = size - 2;i >= 0;--i)
        {
            maxRight[i] = max(height[i], maxRight[i + 1]);
        }

5.思考是否可以进行空间的优化

在下一个思路进行空间优化

代码
class Solution 
{
public: 
    //动态规划
    int trap(vector<int>& height)
    {
        int sz = height.size();
        if (sz == 0) return 0;

        vector<int> maxLeft(sz);
        vector<int> maxRight(sz);

        //maxLeft[i]表示当前位置i的左边历史最高柱(包括i)
        maxLeft[0] = height[0];
        for (int i = 1; i < sz; ++i)
        {
            maxLeft[i] = max(maxLeft[i - 1], height[i]);
        }

        //maxRight[i]表示当前位置i的右边历史最高柱(包括i)
        maxRight[sz - 1] = height[sz - 1];
        for (int i = sz - 2; i >= 0; --i)
        {
            maxRight[i] = max(maxRight[i + 1], height[i]);
        }

        int result = 0;
        for (int i = 1; i < sz - 1; ++i)
        {
            result += min(maxLeft[i], maxRight[i]) - height[i];
        }

        return result;
    }
}
复杂度分析
  • 时间复杂度:O(N )
  • 空间复杂度:O(N)
思路(优化动态规划:双指针法)

设置双指针left = 0,right = sz - 1

  • 不妨假设一开始height[left]小于height[right],那么left会一直向右移动,直到height[left]大于height[right]。在这段时间里,left遍历的所有点都是左侧最高点maxLeft小于右侧最高点maxRight的,所以只需要判断maxLeft与当前高度的关系就行。
  • 当height[left]大于height[right],那么right会一直向右移动,直到height[left]小于height[right]。在这段时间里,right遍历的所有点都是左侧最高点maxLeft大于右侧最高点maxRight的,所以只需要判断maxRight与当前高度的关系就行。
代码
class Solution 
{
public: 
    //优化动态规划: 双指针法
    /*假设一开始height[left]小于height[right],则之后left会一直向右移动,直到height[left]大于           height[right]。在这段时间,left所遍历的所有点都是左侧最高点maxLeft小于右侧最高点maxRight的,所以       只需要判断maxLeft与当前高度的关系就行。反之亦然。*/
    int trap(vector<int>& height)
    {
        int sz = height.size();
        if (sz == 0) return 0;

        int result = 0;
        int left = 0, right = sz - 1;
        int maxLeft = 0, maxRight = 0;
        while (left <= right)
        {
            if (height[left] < height[right])
            {
                if (height[left] >= maxLeft)
                {
                    maxLeft = height[left];
                }
                else
                {
                    result += maxLeft - height[left];
                }
                ++left;
            }
            else
            {
                if (height[right] >= maxRight)
                {
                    maxRight = height[right];
                }
                else
                {
                    result += maxRight - height[right];
                }
                --right;
            }
        }

        return result;
    }
}
复杂度分析
  • 时间复杂度:O(N)
  • 空间复杂度:O(1)
思路(单调栈)

首先思维要从上面三种方法“列看待”转换为“行看待”

根据题意可得:只有当前柱左右两边都有比自己高的柱子才能接到雨水。我们很自然想到可以使用单调栈来帮助找到当前柱左右两边比自己高的柱。

回顾一下单调栈的性质(以下讨论针对单调递减栈):

  • 当元素出栈时,说明这个新元素是出栈元素向后找第一个比其小大的元素
  • 当元素出栈后,说明新栈顶元素是出栈元素向前找第一个比其大的元素

所以我们可以用一个单调递减栈找到当前柱左右两边比自己高的柱。

代码
class Solution 
{
public: 
    //单调栈
    //首先思维要从上面三种方法“列看待”转换为“行看待”
    //只有当前柱左右两边都有比自己高的柱子才能接到雨水,所以用一个单调递减栈找到当前柱左右两边比自己高的柱
    int trap(vector<int>& height)
    {
        int sz = height.size();
        if (sz == 0) return 0;

        int result = 0;
        stack<int> stk;
        for (int i = 0; i < sz; ++i)
        {
            while (!stk.empty() && height[i] > height[stk.top()])
            {
                int cur = stk.top();
                stk.pop();
                if (stk.empty())
                {
                    break;
                }

                int right = i;
                int left = stk.top();
                int width = right - left - 1;
                int high = min(height[right], height[left]) - height[cur];
                result += width * high;
            }
            
            stk.push(i);
        }

        return result;
    }
};
复杂度分析
  • 时间复杂度:O(N)
  • 空间复杂度:O(1)

LeetCode221. 最大正方形

https://leetcode-cn.com/problems/maximal-square/

在这里插入图片描述

输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:4
思路

1.思考状态:找出最优解的性质,明确状态表示什么

dp[i] [j] 表示以matrix[i] [j] (值为1)为右下角的全1正方形的边长

2.思考状态转移方程:大问题的最有解如何由小问题的最优解得到

若matrix[i] [j]为 1,则以此为右下角的全1正方形的最大边长为:上面的全1正方形、左面的全1正方形或左上的全1正方形中,边长最小的那个加1 。换个角度来看:当前格、上、左、左上都不能受 0 的限制,才能成为全1正方形。

在这里插入图片描述

  • 图 1:受限于左上的 0
  • 图 2:受限于上边的 0
  • 图 3:受限于左边的 0

所以:if (matrix[i][j] == ‘1’): dp[i] [j] = min(dp[i - 1] [j - 1], dp[i - 1] [j], dp[i] [j - 1]) + 1

3.思考初始状态

由于第0行和第0列都无法延展,

所以: if (matrix[0] [j] = 1) dp[0] [j] = 1
if (matrix[i] [0] = 1) dp[i] [0] = 1

4.自底向上计算得到最优解

5.思考是否可以进行空间的优化

代码
class Solution 
{
public:
    int maximalSquare(vector<vector<char>>& matrix) 
    {
        if (matrix.size() == 0) return 0;
        int rows = matrix.size();
        int cols = matrix[0].size();

        vector<vector<int>> dp(rows, vector<int>(cols, 0));

        int side = 0;
        for (int i = 0; i < rows; ++i)
        {
            for (int j = 0; j < cols; ++j)
            {
                if (matrix[i][j] == '1')
                {
                    if (i == 0 || j == 0)
                    {
                        dp[i][j] = 1;
                    }
                    else
                    {
                        dp[i][j] = min(min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
                    }

                    side = max(side, dp[i][j]);   
                }   
            }
        }

        return side * side; 
    }
};

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

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

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

输入:matrix =
[
  [0,1,1,1],
  [1,1,1,1],
  [0,1,1,1]
]
输出:15
解释: 
边长为 1 的正方形有 10 个。
边长为 2 的正方形有 4 个。
边长为 3 的正方形有 1 个。
正方形的总数 = 10 + 4 + 1 = 15.
思路

本题与LeetCode221. 最大正方形思路完全一致(见上文)。唯一不同的点是所求的结果:

  • LeetCode221. 最大正方形求的是最大正方形的面积,所以在最内层循环中需要记录最大正方形的边长。
  • 本题求的是统计全为1的子正方形的个数,所以在最内层循环中需要把个数加到最终结果上。
代码
class Solution 
{
public:
    int countSquares(vector<vector<int>>& matrix) 
    {
        if (matrix.size() == 0) return 0;

        int rows = matrix.size();
        int cols = matrix[0].size();

        vector<vector<int>> dp(rows, vector<int>(cols, 0));

        int result = 0;
        for (int i = 0;i < rows;++i)
        {
            for (int j = 0;j < cols;++j)
            {
                if (matrix[i][j] == 1)
                {
                    if (i == 0 || j == 0)
                    {
                        dp[i][j] = 1;
                    }
                    else
                    {
                        dp[i][j] = min(min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
                    }

                    result = result + dp[i][j];
                }   
            }
        }

        return result;
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值