【leetcode-Python】-Dynamic Programming-42. Trapping Rain Water

目录

 

题目链接

题目描述

示例一

示例二

解题思路一· 暴力求解(TLE)

Python实现

解题思路二·动态规划

Python实现

时间复杂度与空间复杂度

解题思路三·单调栈

举例说明

Python实现

时间复杂度与空间复杂度

解题思路四·对撞指针法

举例说明

Python实现

时间复杂度与空间复杂度

参考

 


题目链接

https://leetcode.com/problems/trapping-rain-water/

题目描述

给定n个非负整数组成的数组表示宽度为1的各个柱子的高度,计算柱子按照数组顺序排列后能够接多少雨水。

示例一

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]

输出:6

解析:

示例二

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

输出:9

解析:

解题思路一· 暴力求解(TLE)

此题为面试常见的接雨水问题。对于数组中每个元素代表的柱子(按列计算),计算这个柱子的位置上能存多少雨水,各个位置能存的雨水相加就是最后结果。

由于柱子宽度为1,那么每个柱子的位置能接的雨水量就等于雨水高度。而每个柱子的高度+雨水高度=该柱及位于该柱左侧柱子中的最大高度和该柱及位于该柱右侧柱子中最大高度中的最小值。那么有

对应位置的雨水高度=Min(该柱及位于该柱左侧柱子中的最大高度,该柱及位于该柱右侧柱子中的最大高度)-该柱高度。

Python实现

i表示当前计算的柱子索引,对于每个i,计算[0,i]范围内柱子高度的最大值和[i,len(height)-1]范围内柱子高度的最大值,取其中较小的一个减去当前柱子的高度,就是当前柱子能存水的量。下面实现时间复杂度为O(n^2),空间复杂度为O(1)。但在线提交时会报TLE错误,因此需要进一步优化时间复杂度。

class Solution:
    def trap(self, height: List[int]) -> int:
        result = 0
        for i in range(1,len(height)-1):
            #对于每个i重置max_left,max_right
            max_left = 0
            max_right = 0
            #查找该柱及其左边柱子的最大高度
            for j in range(0,i+1):
                max_left = max(max_left,height[j])
            #查找该柱及其右边柱子的最大高度
            for k in range(i,len(height)):
                max_right = max(max_right,height[k])
            cur_rain_height = min(max_left,max_right)-height[i] #该柱最大能存储的雨水值
            result += cur_rain_height
        return result
        

解题思路二·动态规划

由于在暴力解法中,对于每个柱子i都要对i前面和i后面的柱子进行遍历,这中间涉及到很多重复计算。如果柱子i的左侧最大高度被计算出来并存储,那么柱子i+1的左侧最大高度可以在此基础上进行计算。如果柱子i+1的右侧最大高度被计算出来并存储,那么柱子i的右侧最大高度可以在此基础上进行计算。

用max_left[i]存储最左边柱子到柱子i的最大高度,max_right[i]存储柱子i到最右边柱子的最大高度。可以得到递推式:

max_left[i+1] = max(max_left[i],height[i+1])

max_right[i] = max(max_right[i+1],height[i])

因此max_left数组需要按照索引从小到大的顺序更新,max_right数组需要按照索引从大到小的顺序更新

初始条件为max_left[0] = height[0],max_right[len(height)-1] = height[len(height)-1]

将max_left数组和max_right数组更新完毕后,再遍历一遍height数组,把各个柱子位置存的雨水高度计算出来累加。

Python实现

class Solution:
    def trap(self, height: List[int]) -> int:
        result = 0
        #面试时要提前考虑好边缘例
        if not height:#在Python中,False,0,'',[],{},()都可以视为假。
            return 0
        max_left = [0 for _ in range(len(height))]
        max_right = [0 for _ in range(len(height))]
        #初始化
        max_left[0] = height[0]
        max_right[-1] = height[-1]
        for i in range(1,len(height)):
            max_left[i] = max(max_left[i-1],height[i])
        for j in range(len(height)-2,-1,-1):
            max_right[j] = max(max_right[j+1],height[j])
        for i in range(0,len(height)):
            cur_rain_height = min(max_left[i],max_right[i]) - height[i]
            result += cur_rain_height
        return result

时间复杂度与空间复杂度

时间复杂度为O(n),空间复杂度为O(n)。

解题思路三·单调栈

计算积水的多少不仅可以按列计算,还可以按行计算。如果几个柱子之间想要存水,就一定会出现积水,即两边高,中间低的部分,凹槽的底就是中间比较矮的柱子。我们可以通过维护一个单调递减栈(从栈底到栈顶,元素从大到小排列)来识别凹槽。从左到右遍历柱子的高度,如果当前柱子cur的高度小于等于栈顶柱子的高度,则将cur压入栈(说明这里可能有积水)。如果当前柱子cur的高度大于栈顶柱子的高度,就可以暂停计算这一层有多少积水:将位于栈顶的柱子弹出,并将高度记为top(同时是凹槽的高度),那么这一层水的高度为min(此时栈顶柱子高度,当前柱子cur的高度)-top,宽度为此时栈顶柱子和当前柱子cur之间的距离。之后继续比较当前柱子的高度和此时栈顶柱子的高度,判断是否这中间还有积水。如果当前柱子的高度大于此时栈顶柱子的高度,就继续计算积水。否则就把当前柱子压入栈,并继续往后遍历。

举例说明

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

由于4>2>0,因此4,2,0将依次入栈,cur指向下一个柱子:3。

由于3>栈顶元素,那么可以构成一个以0为底的凹槽,将0弹出记录为top,此时栈顶元素为2,水的高度为min(cur,栈顶元素)-top = 2,水的宽度为cur和栈顶元素之间的距离,即1,因此可以得到这一层的水量为2*1=2。

继续比较cur和此时栈顶元素2,由于3>2,因此将栈顶元素2弹出并记为top,此时2就是这一层凹槽的高度,此时栈顶元素为4,因此这一层水的高度为min(cur,栈顶元素)-top=min(3,4)-2 = 1,水的宽度为cur和4之间的距离,为2。因此这一层的积水量为1*2 = 2。

由于cur指向的元素3小于此时的栈顶元素4,因此将3压入栈。cur指向2,由于2<栈顶元素3,将2压入栈。cur指向5。

由于2<5,因此需要2所代表的柱子代表了一段凹槽的高度,将2弹出并记为top,水的高度为min(cur,此时栈顶元素)-top = min(5,3)-2 = 1,水的宽度为cur和此时栈顶元素表示柱子之间的距离,即为1。因此积水量为1*1 = 1。

cur指向的元素继续和栈顶元素比较,由于5>3,因此3弹出记为top,作为这一层凹槽的高度,那么水的高度为min(cur,此时栈顶元素)-top = min(5,4)-3 = 1,水的宽度为cur和此时栈顶元素之间的距离,即为4,因此这一层的积水量为1*4=4。

由于cur指向的5大于栈顶元素4,因此4弹出,但是此时栈为空,因此无法比较5和此时的栈顶元素,因此跳出比较的循环,将5压入栈.此时height数组的遍历也结束了.

各层积水量加起来为最终结果。

由于需要计算当前指向元素和栈顶元素之间的距离,因此我们可以进行简单的修改:在栈中存储柱子的索引,那么索引为i和j(j>i)的柱子之间的距离为j-i-1,这样柱子高度也方便通过索引得到。此外,在弹出栈顶元素后要判断一下栈是否为空,如果栈为空的话,就需要跳出循环,将cur压入栈中,从cur开始继续遍历。

Python实现

class Solution:
    def trap(self, height: List[int]) -> int:
        result = 0
        #维护一个单调递减栈
        stack = []
        for cur in range(len(height)):
            #栈不为空且当前元素大于栈顶元素时
            while(stack and height[cur] > height[stack[-1]]):
                # pop() 函数用于移除列表中的一个元素(默认最后一个元素)
                top = stack.pop() #取出栈顶元素作为凹槽的底部高度,top为索引
                if(not stack):
                    break
                water_height = min(height[cur],height[stack[-1]])-height[top]
                width = cur - stack[-1] - 1
                result += water_height * width
            stack.append(cur)
        return result
                

时间复杂度与空间复杂度

遍历一遍数组,在遍历过程中每个元素最多有进栈,出栈两种操作,因此时间复杂度为O(n)。由于需要用到栈,最坏情况下(成阶梯状或高度都相等的柱子)的空间复杂度为O(n)。

解题思路四·对撞指针法

仍然是按列计算,在解题思路二中,我们对每个柱子i都求了该柱子左边最大值left_max[i]和右边最大值right_max[i],并得到该位置的积水高度:min(left_max[i],right_max[i])-height[i]。可以看出积水高度只和left_max[i]和right_max[i]中的最小值和当前柱的高度有关。

如果left_max[i]>right_max[i],那么积水高度将由right_max[i]和height[i]决定,如果left_max[i]<right_max[i],那么积水高度将由left_max[i]和height[i]决定.

因此可以通过对撞指针仅对数组进行一次遍历,计算各个柱子位置能够存水的高度.由left_max变量存储[0,...left]范围内的最高柱子的高度,由right_max变量存储[right,end]范围内最高柱子的高度.如果某一端(如右端)有更高的柱子(right_max > left_max),那么就固定这一端,从更低的一边(即从左往右)开始计算积水,反之类似. 

更具体一些,当right_max>=left_max时,柱子left能存的水的高度为left_max-height[left],和其他柱子的高度没有关系.当left_max > right_max时,柱子right能存的水的高度为right_max-height[right].

举例说明

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]

1、初始化left_max和right_max为0,初始化表示总积水量的变量result=0。

2、初始化变量left = 0,right = len(height)-1,更新left_max=0和right_max=1。由于满足left_max <= right_max,因此开始从左往右计算积水: result += left_max - height[left](result更新后仍为0),left右移一位。

3、更新left_max为1,right_max仍为1,由于满足left_max <= right_max,因此继续从左往右计算积水: result += left_max - height[left](result更新后仍为0),left右移一位。

 

 

4、left_max和right_max没有改变,仍有left_max <= right_max,因此继续从左往右计算积水: result += left_max - height[left]=1,left右移一位。

 

5、更新left_max为2,由于left_max > right_max,因此从右往左计算积水: result += right_max - height[right],result仍然为1,right左移一位。

6、更新right_max为2,由于left_max <= right_max,因此从左往右计算积水: result += left_max - height[left],result仍然为1,left右移一位。

7、 left_max和right_max不变,仍有left_max <= right_max,因此从左往右计算积水: result += left_max - height[left](1+1 = 2),left右移一位。

8、 left_max和right_max不变,仍有left_max <= right_max,因此从左往右计算积水: result += left_max - height[left](2+2 = 4),left右移一位。

9、left_max和right_max不变,仍有left_max <= right_max,因此从左往右计算积水: result += left_max - height[left](4+1=5),left右移一位。

9、更新left_max=3,right_max仍为2,由于有left_max > right_max,因此固定住左边,从右往左计算积水: result += right_max - height[right](值不变),right左移一位。

10、left_max和right_max不变,由于有left_max > right_max,因此仍从右往左计算积水: result += right_max - height[right](5+1=6),right左移一位。

11、left_max和right_max不变,由于有left_max > right_max,因此仍从右往左计算积水: result += right_max - height[right](值不变),right左移一位。

此时left和right相等,并指向高度最高的柱子,在这个柱子的位置一定不会积水,因此可以跳出循环。

简单总结一下算法的思想,比较到当前为止的左边最高柱和右边最高柱,哪边低就去更新哪边。假如左边低,那么更新左边,将当前柱和当前左边最高柱相差的高度用水填满,假如右边低,那么更新右边,将当前柱和当前右边最高柱相差的高度用水填满。

Python实现

class Solution:
    def trap(self, height: List[int]) -> int:
        left,right,left_max,right_max = 0,len(height)-1,0,0
        result = 0 #存储最终结果
        while(left<right):
            left_max = max(height[left],left_max)
            right_max = max(height[right],right_max)
            if(left_max <= right_max):
                result += (left_max-height[left])
                left += 1
            else:
                result += (right_max - height[right])
                right -= 1
        return result
                

时间复杂度与空间复杂度

由于需要遍历一次数组,因此时间复杂度为O(n).由于过程中仅维护了left_max和right_max两个变量,因此空间复杂度为O(1).

参考

https://leetcode-cn.com/problems/trapping-rain-water/solution/42-jie-yu-shui-shuang-zhi-zhen-dong-tai-gui-hua-da/

https://leetcode-cn.com/problems/trapping-rain-water/solution/xiang-xi-tong-su-de-si-lu-fen-xi-duo-jie-fa-by-w-8/

https://leetcode-cn.com/problems/trapping-rain-water/solution/42-jie-yu-shui-shuang-zhi-zhen-dong-tai-wguic/

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值