Leetcode 42 - 接雨水(三种方法)

我的原文链接:http://ben-personal.top/2020/04/leetcode-42-traprain/

这道题将对比三种方法,分别是动态规划、双指针(改进的动态规划)和单调栈法。通过这道题至少可以感受到两方面的进步:

  • 看问题的视角不同,形成的算法也完全不同
  • 动态规划常常可以进行空间上的改进

题目如下:

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

数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图如下,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。


示例

解法一 动态规划

这一方法的基本思想在于,任一位置的存雨量取决于其左右最高柱子的较小者,这也是所谓木桶效应,最终值应为:

min{左边最高柱子高度,右边最高柱子高度} - 该位置柱子高度

那这和动态规划有何关系呢?关键就在于左右最高柱子的高度获取上。如果迭代到每个位置都重新计算一遍左右最高柱子,显然是不合算的。如果能发现相邻位置之间的如下关系:

# 记H(i)为柱子i左边最高的柱子高度,则
H(i+1)=max{柱子i的高度,H(i)}

那就意味着不必每次重新计算,如果存储下这些数据,则可节省大量的时间。
由此诞生动态规划法,用两个数组存下每个位置左右的最大高度即可:

  def trap(self, height: List[int]) -> int:
        num_cols = len(height)
        if num_cols < 3:
            return 0
        ans = 0
        h_left = [0]
        h_right = [0] #反过来的
        for i in range(num_cols-1):
            h_left.append(max(height[i], h_left[-1]))
            h_right.append(max(height[num_cols-1-i], h_right[-1]))

        for i in range(num_cols):
            ans += max(0, min(h_left[i], h_right[num_cols-1-i]) - height[i])
        return ans

解法二 双指针法

前面说过,这种方法又称为改进的动态规划法,是因为它就是在解法一的基础上衍生出来的。由于解法一中,数组中存储的每个数值只用过一次就不再使用,那能不能在用的时候算一次就好呢?

用两个指针从左右同时出发,这样可以分别获得左侧和右侧的最高柱子高度。由于存雨量取决左右最高柱子高度的较小者,因此,对于左指针所指的位置来说,一旦其左侧最大值比右侧的已知的最高高度小,则可以判定其左侧最大值比右侧的真实的最高高度(目前未知)小。这时左指针所指位置的雨量可以计算(如图蓝色部分),右侧同理。


ill.png
    def trap(self, height: List[int]) -> int:
        ans = 0
        left = 0 # 左指针的index
        h_left = 0  # 左侧最高的高度
        right = len(height) - 1 # 右指针的index
        h_right = 0  # 右侧最高的高度
        while left <= right:
            if h_left > h_right:  # 意味着h_left也 >right,也 >h_right
                h_right = max(height[right], h_right)
                ans += h_right - height[right]
                right -= 1
            else:
                h_left = max(height[left], h_left)
                ans += h_left - height[left]
                left += 1
        return ans

解法三 单调栈法

这种方法相对难以理解一些,不过提供了另一个看问题的角度。前两种方法是逐列统计雨量的,而单调栈法则是分块逐层统计雨量。

所谓单调栈,是指栈中元素是绝对有序的。当即将入栈的值不符合栈中的顺序时,则需要将栈顶元素出栈,直到能够满足顺序时才能入栈。

就本题而言,考虑一个单调递减的栈,即栈顶元素最小。因为当后入栈的柱子高度较小时,是不可能存下雨水的(见下图)。只有当即将入栈的柱子高度比栈顶的大时,才开始存下雨水。


ill.png

那具体存下了多少呢?从栈顶即将出栈的柱子来看(图中右侧第二个),雨量加上本身的高度不能超过左右柱子中较低者,故雨量为深蓝部分。

当此柱子出栈后,继续比较栈顶元素与即将入栈的柱子高度,发现仍然小,那仍然可以存下雨水,其雨量为(左右柱子中较低者-本身的高度)*2,即浅蓝部分。为什么乘2?从图中容易看出,上一个出栈的元素并没有考虑到这一部分雨量。

直到栈顶柱子比即将入栈的柱子高或栈为空时,将此柱子入栈。总的来说,从图像可以看出,这是分层计算雨量,与之前的解法角度明显不同。实现代码如下:

def trap(self, height: List[int]) -> int:
        if not height:
            return 0
        stack = []  # 存储高度的index
        ans = 0 # 结果雨水量
        for i in range(len(height)):
            if stack:
                if height[i] <= height[stack[-1]]:
                    stack.append(i)
                else:
                    while height[i] > height[stack[-1]]:
                        stackTop = stack.pop()
                        while stack and height[stack[-1]] == height[stackTop]:
                            stackTop = stack.pop()
                        if stack:
                            ans += (i-stack[-1]-1) * (min(height[i], height[stack[-1]])-height[stackTop])
                        else:
                            break
                    stack.append(i)
                    
            else:
                if height[i] != 0:
                    stack.append(i)
        return ans

通过对动态规划的改进,我们发现,当动态规划的数组中元素利用率低时,常常可以改进算法,节省空间。同时,通过本问题,也可以了解单调栈的应用,类似的数据结构还有单调队列,可以自行学习。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,941评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,397评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,345评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,851评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,868评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,688评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,414评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,319评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,775评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,945评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,096评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,789评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,437评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,993评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,107评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,308评论 3 372
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,037评论 2 355

推荐阅读更多精彩内容

  • 题目描述(困难难度) 黑色的看成墙,蓝色的看成水,宽度一样,给定一个数组,每个数代表从左到右墙的高度,求出能装多少...
    windliang阅读 570评论 0 0
  • 题目描述 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 上面...
    算法码上来阅读 1,263评论 0 0
  • 题目描述(困难难度) 给一个柱状图,输出一个矩形区域的最大面积。 解法一 暴力破解 以题目给出的例子为例,柱形图高...
    windliang阅读 616评论 0 0
  • 题目:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 解法一 ...
    关山Kwan阅读 357评论 0 0
  • 1. 桥头文件已经删除,工程运行出错,可在此处删除桥头文件。 1.在Build Setting -> Object...
    yangliangliang阅读 76评论 0 0