2022-11-16 每日打卡:单调栈解决最大矩形问题(一维直方图,二维最大红矩形)

每日打卡:单调栈解决最大矩形问题(一维直方图,二维最大红矩形)

柱状图中最大的矩形

2022-11-16 每日打卡:单调栈解决最大矩形问题(一维直方图,二维最大红矩形)_第1张图片

思路

这个题最明显的思路就是:矩形面积=底×高。

  • 版本1:底的长度可以通过二重循环来完成,高通过循环来寻找最小值。
  • 版本2:底的长度可以通过二重循环来完成,高通过线性比较当前遍历到的高度和之前记录的max(minh, h)

可以发现,通过遍历底×高的方法来做,优化的上限就到此为止。那么我们想一想,能否通过遍历高×底的思想呢?

如果能通过遍历每个高,这只需要O(n)的时间复杂度,同时找到以这个高度为高的最大矩形的底有多长就好了!

考虑对某一个高而言,它的底究竟是什么?如下图(参考链接)所示:
2022-11-16 每日打卡:单调栈解决最大矩形问题(一维直方图,二维最大红矩形)_第2张图片

也就是我们如果能在遍历到下标为2的柱子的时候,得到这两个信息就好了:

  • 左侧比他矮的最近的柱子的下标,也就是1
  • 右侧比他矮的最近的柱子的下标,也就是4

我们想到一种结构:单调栈可以存储单调递增/递减的柱子下标。这样上面提到的第一个信息,也就是遍历时左侧比他矮的最近的柱子下标就知道了。

那么右边的呢?

右边的相当于现在的高度而言其实是“未来”的,也就是还没遍历到的。那我们考虑在“未来”的遍历过程中,无非是出现“比现在这个高度高” 和 “比现在这个高度低” 两种情况,边界的等于我们稍后再讨论。

当遇到比现在高度高时,就需要添加到栈中。因为栈必须维护 “前一个存储的下标是后一个存储下标的左边界”

当遇到比现在高度低的时候,其实就是上面提到的第二个需要知道的信息,右侧比他矮的第一个柱子的下标!

下面我们来整理一下思路:

  • 遍历每个高度,记作height
  • 单调栈维护左侧比height矮的最近的柱子的下标。称为lo。
  • 当右侧出现第一个比height矮的柱子时,计算height×底,此时底的长度是(hi-lo-1)。

而在代码实现的过程中,因为栈具有记录功能,所以上面我们所说的遍历每个高度,计算面积的过程其实不是在当前高度被遍历到的时候计算的,因为此时hi的值不知道。

我们使用栈记录了额外的信息,也就是待计算的高度height。把真正计算面积,更新最大面积的过程延迟到了hi出现的时候!我认为这才是栈作为数据结构之所以有用的原因。

而单调栈则相比栈记录了更多的信息,也就是比height矮的最近的柱子的下标lo就是height的前一个位置。

代码实现

class Solution:
    def largestRectangleArea(self, heights: List[int]) -> int:
        ans, stack = 0, []

        # 添加哨兵
        heights = [0]+heights
        heights.append(0)

        # 左侧限位lo, 右侧限位hi
        lo, hi = 0, 0
        for hi, tmp in enumerate(heights):
        	# 注意边界条件,栈为空时直接入栈
            while (stack and tmp<heights[stack[-1]]):
                height = heights[stack.pop()]
                lo = stack[-1]
                ans = max(ans, height*(hi-lo-1))
            stack.append(hi)
        return(ans)

复杂度分析

时间复杂度

在遍历数组时,直方图的每根柱子都入栈,出栈一次,并且在每个柱子的下标出栈时计算以它为顶的最大矩形面积,这些操作对每根柱子而言复杂度都是O(1),故总的时间复杂度为O(n)

空间复杂度

需要一个辅助栈,栈中可能有O(n)根柱子在数组中的下标,故空间复杂度为O(n)

矩阵中最大的矩形

有了上面的铺垫,将一维的情况扩展至二维即可。如题:
2022-11-16 每日打卡:单调栈解决最大矩形问题(一维直方图,二维最大红矩形)_第3张图片

如果把每一行/每一列看作一个heights数组,那么问题就可解了。

考虑每一行/每一列的heights应该是多少?

这里以行为例,对于第一行而言,heights应该是【10100】,第二行,则是【20211】,我们可以通过动态规划的方法更新该数组,规律为:

  • 如果位置为1,则height+1
  • 否则height变成0

为什么是正确的?因为最大的矩阵的高度永远被限制在最小的height中,所以即使我们看似加入了y轴以下的部分(有的height出现的位置早,有的出现的位置晚),也会被最小的height限制住。

代码非常简单,只是在每次调用计算柱状图中最大的矩形的外面,加了新的循环,用于更新heights数组。

class Solution:
    def maximalRectangle(self, matrix: List[str]) -> int:
        if not matrix:
            return 0
        col_len = len(matrix[0])
        
        # 记录当前行每一个“柱子”的高度,0和最后一位是哨兵
        heights = [0 for i in range(col_len+2)]
        ans, stack = 0, []
        for line in matrix:
            for i in range(col_len):
                # 如果是1,则长度为上一行长度+1,否则为0
                heights[i+1] = heights[i+1]+1 if line[i]!='0' else 0

            # 栈清空
            stack.clear()
            # 左侧限位lo, 右侧限位hi
            lo, hi = 0, 0
            for hi, tmp in enumerate(heights):
                # 注意边界条件,栈为空时直接入栈
                while (stack and tmp<heights[stack[-1]]):
                    height = heights[stack.pop()]
                    lo = stack[-1]
                    ans = max(ans, height*(hi-lo-1))
                stack.append(hi)
        return(ans)

你可能感兴趣的:(algorithm,算法,leetcode,数据结构)