42. 接雨水

153 阅读6分钟

【题目】

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

示例 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 个单位的雨水(蓝色部分表示雨水)。 

示例 2:

输入: height = [4,2,0,3,2,5]
输出: 9

提示:

  • n == height.length
  • 1 <= n <= 2 * 10^4
  • 0 <= height[i] <= 10^5

【题目解析】

双指针

算法思想:利用两个指针从数组的两端向中间遍历,根据左右两侧最高柱子的高度来决定当前位置能接多少雨水。关键在于,任何位置上能接的雨水量由其左右两侧最高的柱子中较矮的那个决定。

操作细节

  • 初始化两个指针leftright分别指向数组的开头和结尾。
  • 两个变量left_maxright_max用来记录遍历过程中左侧和右侧遇到的最高柱子。
  • 向中间移动leftright指针,根据left_maxright_max的比较结果来决定是移动left还是right,并计算可能的雨水量。

优点:空间复杂度低(O(1)),直接在输入数组上操作,不需要额外的存储空间。

缺点:需要充分理解和利用左右最大值的性质,对初学者来说可能稍微复杂。

class Solution:
    def trap(self, height: List[int]) -> int:
        left, right = 0, len(height) - 1
        left_max, right_max = 0, 0
        water = 0
        
        while left < right:
            if height[left] < height[right]:
                if height[left] >= left_max:
                    left_max = height[left]
                else:
                    water += left_max - height[left]
                left += 1
            else:
                if height[right] >= right_max:
                    right_max = height[right]
                else:
                    water += right_max - height[right]
                right -= 1
                
        return water

image.png

动态规划

算法思想:预先计算并存储每个位置左右两侧的最大高度。这样,在计算每个位置可以接多少雨水时,可以直接使用这些预计算的值,而不需要每次都重新计算。

操作细节

  • 创建两个数组left_maxright_max,分别用于存储从左到右和从右到左遍历时各位置的最大高度。
  • 第一遍扫描数组填充left_max数组,第二遍反向扫描填充right_max数组。
  • 第三遍遍历数组,使用left_maxright_max计算每个位置的雨水量。

优点:通过预计算最大高度,减少了重复计算,提高了效率。

缺点:需要O(n)的额外空间来存储左右最大高度的信息。

class Solution:
    def trap(self, height: List[int]) -> int:
        if not height:
            return 0
        
        n = len(height)
        left_max, right_max = [0] * n, [0] * n
        water = 0

        left_max[0], right_max[n-1] = height[0], height[n-1]
        for i in range(1, n):
            left_max[i] = max(left_max[i-1], height[i])
        for i in range(n-2, -1, -1):
            right_max[i] = max(right_max[i+1], height[i])
        
        for i in range(n):
            water += min(left_max[i], right_max[i]) - height[i]
        
        return water

image.png

算法思想:使用栈来跟踪可能形成低洼地形的柱子的索引,当遇到一个能够形成低洼的柱子时,使用栈顶元素计算雨水量。

操作细节

  • 遍历数组,使用栈存储可能积水的柱子的索引。
  • 当当前柱子高度大于栈顶索引对应的柱子高度时,说明找到了一个低洼,可以接雨水。
  • 计算雨水量,需要知道低洼的宽度和深度,宽度由栈顶元素和当前索引决定,深度由栈顶元素和当前元素的高度差决定。
  • 栈中存储的是索引,而不是高度,这使得计算宽度变得简单。

优点:直观地模拟了积水过程,易于理解。

缺点:需要使用额外的栈空间,空间复杂度为O(n)。

class Solution:
    def trap(self, height: List[int]) -> int:
        stack = []
        water = 0
        for i, h in enumerate(height):
            while stack and height[stack[-1]] < h:
                top = stack.pop()
                if not stack:
                    break
                distance = i - stack[-1] - 1
                bounded_height = min(h, height[stack[-1]]) - height[top]
                water += distance * bounded_height
            stack.append(i)
        return water

image.png

【总结】

适用问题类型

这类问题涉及到数组内部结构的分析,特别是涉及到局部最大值和最小值的比较,以及对数组索引间隔内的数据处理。它适合用于解决需要计算区间内部状态或积累量的问题,如接雨水、股票买卖最佳时机等。

解决算法

双指针法
  • 适用场景:需要在一次遍历内解决问题,对空间复杂度有严格要求的场景。
  • 算法特点:空间复杂度低(O(1)),不需要额外的存储空间,通过左右两端的相对高度差计算积水量。
  • 优化与实践意义:双指针法在多个问题上都有广泛应用,尤其适合于空间敏感的应用场景。通过精确控制两个指针的移动,可以优化问题解决策略,提高代码执行效率。
动态规划
  • 适用场景:适用于需要多次利用同一计算结果的问题,减少重复计算。
  • 算法特点:通过预计算并存储中间结果(左右最大高度)来避免重复计算,空间复杂度为O(n)。
  • 优化与实践意义:动态规划是解决复杂问题的强大工具,特别是当问题可以分解为重叠子问题时。在实践中,动态规划可以用来解决范围广泛的问题,从序列分析到资源分配等。
  • 适用场景:适用于需要追踪和处理输入数据中的非连续结构或模式的问题。
  • 算法特点:使用栈来模拟水的积累过程,每次找到可以积水的凹槽并计算积水量。空间复杂度为O(n)。
  • 优化与实践意义:栈方法在处理包含嵌套或连续结构的数据时特别有用,如编程语言的解析器、HTML和XML的解析等。它提供了一种有效的方式来处理这些结构,并且可以通过优化数据入栈和出栈的逻辑来进一步提高效率。

总结

接雨水问题展示了如何通过不同的算法解决同一问题,并揭示了每种方法的优势和局限性。选择哪种方法取决于问题的具体要求和上下文:

  • 对于对执行时间和空间都有严格要求的情况,双指针法可能是最优解。
  • 如果问题解决过程中存在大量重复计算,那么动态规划可能更适用。
  • 当问题涉及到复杂的数据结构,特别是需要记录数据状态变化的场景,提供了一种有效的解决方案。

题目链接

接雨水