Leetcode 42.接雨水

1.题目描述

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

Leetcode 42.接雨水_第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[i1],height[i])if1in1rightMax[i]=max(rightMax[i+1],height[i])if0in2

  • 正向遍历数组 height 得到数组 leftMax 的每个元素值,反向遍历数组 height 得到数组 rightMax

    的每个元素值。

  • 遍历结束之后,下标i处能接的雨水量=min(leftMax[i], rightMax[i]) - height[i]

2.2 单调栈

Q1:单调栈内的元素顺序?

维护一个单调栈, 单调栈存储的元素的下标, 栈顶->栈底:递增(小->大)

一旦发现添加的柱子高度大于栈顶元素,此时就会出现凹槽, 栈顶元素就是凹槽底部的柱子, 栈顶的第二个元素就是凹槽左边的柱子, 当前元素就是凹槽右边的柱子。
Leetcode 42.接雨水_第2张图片

Q2:遇到相同柱子如何处理?

遇到相同的元素,更新栈内下标,就是将栈里元素(旧下标)弹出,将新元素(新下标)加入栈中。

举个栗子:height= [4,4,1,3], 如果添加第二个5的时候就应该将第一个5的下标弹出,把第二个5添加到栈中。
Leetcode 42.接雨水_第3张图片

单调栈的处理逻辑:

  • 情况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[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右移一位
    Leetcode 42.接雨水_第4张图片

  • 2.left = 1 right = 11 leftMax = 1 rightMax = 1 此时height[left] = height[right], right左移一位
    Leetcode 42.接雨水_第5张图片

  • 3.left = 1 right = 10 leftMax = 1 rightMax = 2 此时height[left] < height[right], left右移一位
    Leetcode 42.接雨水_第6张图片

  • 4.left = 2 right = 10 leftMax = 1 rightMax = 2 此时height[left] < height[right], left右移一位
    Leetcode 42.接雨水_第7张图片

  • 5.left = 3 right = 10 leftMax = 2 rightMax = 2 此时height[left] = height[right], right左移一位
    Leetcode 42.接雨水_第8张图片

  • 6.left = 3 right = 9 leftMax = 2 rightMax = 2 此时height[left] > height[right], right左移一位
    Leetcode 42.接雨水_第9张图片

  • 7.left = 3 right = 8 leftMax = 2 rightMax = 2 此时height[left] = height[right], right左移一位
    Leetcode 42.接雨水_第10张图片

  • 8.left = 3 right = 7 leftMax = 2 rightMax = 3 此时height[left] < height[right], left右移一位
    Leetcode 42.接雨水_第11张图片

  • 9.left = 4 right = 7 leftMax = 2 rightMax = 3 此时height[left] < height[right], left右移一位
    Leetcode 42.接雨水_第12张图片

  • 10.left = 5 right =7 leftMax = 2 rightMax = 3 此时height[left] < height[right], left右移一位
    Leetcode 42.接雨水_第13张图片

  • 11.left = 6 right =7 leftMax = 2 rightMax = 3 此时height[left] < height[right], left右移一位
    Leetcode 42.接雨水_第14张图片

  • 12.遍历结束,返回结果
    Leetcode 42.接雨水_第15张图片

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/

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