【LeetCode刷题笔记(8-1)】【Python】【接雨水】【动态规划】【困难】
编写通过所有测试案例的代码并不简单,通常需要深思熟虑和理性分析。虽然这些代码能够通过所有的测试案例,但如果不了解代码背后的思考过程,那么这些代码可能并不容易被理解和接受。我编写刷题笔记的初衷,是希望能够与读者们分享一个完整的代码是如何在逐步的理性思考下形成的。我非常欢迎读者的批评和指正,因为我知道我的观点可能并不完全正确,您的反馈将帮助我不断进步。如果我的笔记能给您带来哪怕是一点点的启示,我也会感到非常荣幸。同时,我也希望我的分享能够激发您的灵感和思考,让我们一起在编程的道路上不断前行~
给定 n
个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
通过观察图像,我们可以发现红色方框部分与木桶的形态颇为相似。让我们富有想象力地将最左侧高度为2的柱子视作木桶的左侧板,而将最右侧高度为3的柱子视作木桶的右侧板。基于深入人心的【木桶原理】,一个木桶的装水量总是受限于其最短的那块木板 ==> 若能在上图中探寻到所有“有效的”木桶,并计算这些木桶的装水量再进行汇总,我们便能获得所求的结果。
问题1:如何获取所有“有效的”木桶呢?
首先,对木桶的组成部分有个清晰的定义:木桶分为【左侧板】【底板】和【右侧板】三个部分。
通过观察图像,我们可以发现,从左到右遍历height
数组时,柱子高度会呈现出一种特殊的规律。首先,从左侧板到底板的过程中,柱子高度会逐渐降低,形成一个递减的趋势。接着,从底板到右侧板的过程中,柱子高度则会逐渐升高,形成一个递增的趋势。更为有趣的是,图像中多次出现了这种“递减-递增”的模式转换,每一次的转换都可能构成一个“有效的”木桶。这种观察不仅揭示了数据背后的隐藏规律,也为我们解决问题提供了新的视角和思路。
问题2:如何捕捉到这种“递减-递增”的模式转换呢?
使用单调栈
要捕捉这种递减-递增的模式转换,我们可以使用单调栈。单调栈是一种数据结构,它能够保持栈内元素按照特定的顺序排列。在这个问题中,我们可以使用一个单调递减栈stack
来存储数组 height
的下标。 ==> 用单调栈存储潜在的【左侧板】和【底板】。
细节1: 之所以选择存储元素的“下标”,是为了方便计算“有效”木桶的宽度。通过存储下标,我们可以轻松地获取到木桶的左侧板、底板和右侧板的位置,从而计算出木桶的宽度。
从左往右遍历height
数组,如果新的元素height[i]
比栈顶元素height[stack[-1]]
还大 ? 不满足递减 ? 出现了右侧板,那么此时的栈顶下标top=stack.pop()
表示的就是“桶底”,而栈顶元素height[top]
表示底板距离地面(柱子高度为0视为地面)的高度;倘若stack中存储不止一个下标,那么弹出栈顶下标top
后,stack
一定非空 ? 新的栈顶下标left=stack[-1]
表示的就是“左侧板”所在位置,而新的栈顶元素height[left]
表示左侧板的高度**。当“底板”和“左侧板”都存在时,说明存在“有效木桶”,那么新的元素下标i
可以用变量right
进行表征,即right=i
,表示右侧板所在位置。那么Width = right - left - 1
即为木桶的有效宽度。
细节2: 之所以说栈顶元素height[top]
表示底板距离地面的高度是因为每个底板不一定都在地面上。因此,根据木桶原理,木桶的有效高度Height = min(height[left],height[right]) - height[top]
? 该木桶容纳水的含量 water = Height * Width
。
细节3: 在某些区域,可能会有多个木桶
。就像上图所示,红色框代表的大木桶下面,还隐藏着一个绿色框代表的小木桶。根据我们的生活常识,当给这两个叠在一起的木桶装水时,首先装满的一定是小木桶。当小木桶装满水后,我们可以将小木桶当作大木桶的底板(小木桶的左侧板当作大木桶的底板,可和代码结合理解),继续给大木桶装水。而基于【单调栈】的解法,正好与我们的常识保持一致。
完整代码:
class Solution:
def trap(self, height: List[int]) -> int:
total_water = 0 # 记录水的容量
stack = list() # 创建单调递减栈
n = len(height)
for i, h in enumerate(height): # 遍历数组height的每个元素
# 当栈非空 且 新的元素h比栈顶元素height[stack[-1]]还大时
while stack and h > height[stack[-1]]:
right = i # 找到右侧板,用变量right进行表征
top = stack.pop() # 栈顶下标表示底板,不可能在表示左侧板,更不可能是右侧板 ---> 直接弹出
if not stack: # 栈空 --> 没有找到左木板,不能组成有效的木桶
break # 跳出while循环 寻找下一个右木板
else: # 找到左木板
left = stack[-1] # 当前栈顶表示左木板的下标可能会作为下一个有效木桶的底板 --> 不能弹出
if height[left] == height[top]:
# 左木板高度和底板高度一致 ---> 根据木板原理,装不下任何水
continue # 固定右侧板,寻找下一个有效木桶
else:
# height[top] < height[left] 且 height[top] < height[right] 恒成立
# 这个木桶一定能装到水
bucket_width = right - left - 1
bucket_height = min(height[left], height[right]) - height[top]
total_water += bucket_width * bucket_height
# 当前扮演右木板 --> 接着可能会扮演左木板/桶底
stack.append(i)
return total_water
运行结果:
复杂度分析
height
元素的数量。
height
===> O(N)height
每个元素最多出/入栈一次 ===> O(1)