1.题目描述
给定
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 个单位的雨水(蓝色部分表示雨水)。
输入:height = [4,2,0,3,2,5]
输出:9
- 提示:
n == height.length
1 <= n <= 2 * 104
0 <= height[i] <= 105
2.思路分析
2.1 动态规划
对于下标 i,下雨后水能到达的最大高度等于下标 i 两边的最大高度的最小值,下标 i 处能接的雨水量
等于下标 i 处的水能到达的最大高度减去 height[i]。
当前列的雨水面积= min(左边柱子的最高高度, 右边柱子的最高高度) - 当前柱子的高度
具体做法:
- 创建两个长度为n的数组leftMax和rightMax。
- leftMax[i]:下标i及其左边的位置中height的最大高度
- rightMax[i]:下标i及其右边的位置中height的最大高度
- leftMax[0] = height[0] rightMax[n-1] = height[n-1]
l e f t M a x [ i ] = m a x ( l e f t M a x [ i − 1 ] , h e i g h t [ i ] ) i f 1 ≤ i ≤ n − 1 r i g h t M a x [ i ] = m a x ( r i g h t M a x [ i + 1 ] , h e i g h t [ i ] ) i f 0 ≤ i ≤ n − 2 leftMax[i] = max(leftMax[i-1], height[i]) \qquad if \quad 1 ≤ i ≤ n-1 \\ rightMax[i] = max(rightMax[i+1], height[i]) \qquad if \quad 0 ≤ i ≤ n-2 leftMax[i]=max(leftMax[i−1],height[i])if1≤i≤n−1rightMax[i]=max(rightMax[i+1],height[i])if0≤i≤n−2
-
正向遍历数组 height 得到数组 leftMax 的每个元素值,反向遍历数组 height 得到数组 rightMax
的每个元素值。
-
遍历结束之后,下标i处能接的雨水量=min(leftMax[i], rightMax[i]) - height[i]
2.2 单调栈
Q1:单调栈内的元素顺序?
维护一个单调栈, 单调栈存储的元素的下标, 栈顶->栈底:递增(小->大)
一旦发现添加的柱子高度大于栈顶元素,此时就会出现凹槽, 栈顶元素就是凹槽底部的柱子, 栈顶的第二个元素就是凹槽左边的柱子, 当前元素就是凹槽右边的柱子。
Q2:遇到相同柱子如何处理?
遇到相同的元素,更新栈内下标,就是将栈里元素(旧下标)弹出,将新元素(新下标)加入栈中。
举个栗子:height= [4,4,1,3], 如果添加第二个5的时候就应该将第一个5的下标弹出,把第二个5添加到栈中。
单调栈的处理逻辑:
-
情况1:当前遍历元素高度 < 栈顶元素的高度, 当前元素入栈(保持 小->大 单调的性质 )
-
情况2: 当前遍历元素高度 == 栈顶元素高度, 更新栈顶元素(遇到相同柱子,使用右边柱子计算高度)
-
情况3: 当前遍历元素高度 > 栈顶元素, 此时出现凹槽, 弹出栈顶元素(凹槽底部柱子, 记为mid), 此时
栈顶元素(stack.top())(凹槽左边的柱子, height[st.top()]), 当前元素记为凹槽右边的柱子(height[i])
- 雨水的高度: h = min(height[st.top()], height[i]) - height[mid]
- 雨水的宽度: w = i - stack.top() -1
- 凹槽的体积:h * w
2.3 双指针
维护两个指针 left 和 right, 以及两个变量 leftMax 和 rightMax, 初始时 left=0, right=n-1,
leftMax=0,rightMax=0。指针 left 只会向右移动,指针 right 只会向左移动,在移动指针的过程中
维护两个变量 leftMax 和 rightMax 的值。
当两个指针没有相遇时,
-
使用 height[left] 和height[right] 的值更新 leftMax 和rightMax 的值;
-
如果 height[left]<height[right],则必有 leftMax<rightMax,下标 left 处能接的雨水量等于leftMax−height[left],将下标left 处能接的雨水量加到能接的雨水总量,然后将 left 加 1(即向右移动一位);
-
如果 height[left]≥height[right],则必有 leftMax≥rightMax,下标right 处能接的雨水量等于 rightMax−height[right],将下标 right 处能接的雨水量加到能接的雨水总量,然后将 right 减 1(即向左移动一位)。
当两个指针相遇时,即可得到能接的雨水总量。
举个栗子: height=[0,1,0,2,1,0,1,3,2,1,2,1]
-
1.left = 0 right = 11 leftMax = 0 rightMax = 1 此时height[left] < height[right], left右移一位
-
2.left = 1 right = 11 leftMax = 1 rightMax = 1 此时height[left] = height[right], right左移一位
-
3.left = 1 right = 10 leftMax = 1 rightMax = 2 此时height[left] < height[right], left右移一位
-
4.left = 2 right = 10 leftMax = 1 rightMax = 2 此时height[left] < height[right], left右移一位
-
5.left = 3 right = 10 leftMax = 2 rightMax = 2 此时height[left] = height[right], right左移一位
-
6.left = 3 right = 9 leftMax = 2 rightMax = 2 此时height[left] > height[right], right左移一位
-
7.left = 3 right = 8 leftMax = 2 rightMax = 2 此时height[left] = height[right], right左移一位
-
8.left = 3 right = 7 leftMax = 2 rightMax = 3 此时height[left] < height[right], left右移一位
-
9.left = 4 right = 7 leftMax = 2 rightMax = 3 此时height[left] < height[right], left右移一位
-
10.left = 5 right =7 leftMax = 2 rightMax = 3 此时height[left] < height[right], left右移一位
-
11.left = 6 right =7 leftMax = 2 rightMax = 3 此时height[left] < height[right], left右移一位
-
12.遍历结束,返回结果
3.代码实现
3.1 动态规划
class Solution:
def trap(self, height: List[int]) -> int:
if not height:
return 0
n = len(height)
leftMax = [height[0]] + [0] * (n - 1)
rightMax = [0] * (n - 1) + [height[n - 1]]
for i in range(1, n):
leftMax[i] = max(leftMax[i-1], height[i])
for i in range(n-2, -1, -1):
rightMax[i] = max(rightMax[i+1], height[i])
ans = sum(min(leftMax[i], rightMax[i]) - height[i] for i in range(n))
return ans
复杂度分析
- 时间复杂度:O(n),其中 n 是数组height 的长度。 计算数组 leftMax 和 rightMax 的元素值各需要遍历数组 height 一次,计算能接的雨水总量还需要遍历一次。
- 空间复杂度:O(n),其中 nn 是数组 height 的长度。需要创建两个长度为 n 的数组 leftMax 和 rightMax。
3.2 单调栈
# 方式1
class Solution:
def trap(self, height: List[int]) -> int:
if not height:
return 0
result = 0
stack = [0]
for i in range(1, len(height)):
# 情况1:当前元素 < 栈顶元素
if height[i] < stack[-1]:
stack.append(i)
# 情况2:当前元素 == 栈顶元素
elif height[i] == stack[-1]:
stack.pop()
stack.append(i)
else: # 情况3:当前元素 > 栈顶元素
while stack and height[i] > height[stack[-1]]:
mid_height = height[stack[-1]]
stack.pop()
if stack:
right_height = height[i]
left_height = height[stack[-1]]
h = min(left_height, right_height) - mid_height
w = i - stack[-1] - 1
result += h * w
stack.append(i)
return result
# 方式2
class Solution:
def trap(self, height: List[int]) -> int:
stack = [0]
result = 0
for i in range(1, len(height)):
while stack and height[i] > height[stack[-1]]:
mid_height = stack.pop()
if stack:
# 雨水高度是 min(凹槽左侧高度, 凹槽右侧高度) - 凹槽底部高度
h = min(height[stack[-1]], height[i]) - height[mid_height]
# 雨水宽度是 凹槽右侧的下标 - 凹槽左侧的下标 - 1
w = i - stack[-1] - 1
# 累计总雨水体积
result += h * w
stack.append(i)
return result
复杂度分析
- 时间复杂度:O(n),其中 n 是数组 height 的长度。从 0 到 n-1 的每个下标最多只会入栈和出栈各一次。
- 空间复杂度:O(n),其中 n 是数组 height 的长度。空间复杂度主要取决于栈空间,栈的大小不会超过 n。
3.3 双指针
class Solution:
def trap(self, height: List[int]) -> int:
ans = 0
left, right = 0, len(height) - 1
leftMax = rightMax = 0
while left < right:
leftMax = max(leftMax, height[left])
rightMax = max(rightMax, height[right])
if height[left] < height[right]:
ans += leftMax - height[left]
left += 1
else:
ans += rightMax - height[right]
right -= 1
return ans
# 方式3
class Solution:
def trap(self, height: List[int]) -> int:
if not height:
return 0
result = 0
stack = []
for i in range(len(height)):
while stack and height[i] > height[stack[-1]]:
mid_height = height[stack.pop()]
if stack:
h = min(height[stack[-1]], height[i]) - mid_height
w = i - stack[-1] - 1
result += h * w
stack.append(i)
return result
复杂度分析
- 时间复杂度:O(n),其中 n 是数组 height 的长度。两个指针的移动总次数不超过 n。
- 空间复杂度:O(1)。只需要使用常数的额外空间。
参考:
1.https://leetcode.cn/problems/trapping-rain-water/solution/jie-yu-shui-by-leetcode-solution-tuvc/