0
点赞
收藏
分享

微信扫一扫

Leetcode2086. 从房屋收集雨水需要的最少水桶数(medium)

一点读书 2022-03-30 阅读 45

目录

1. 题目描述

2. 方法一:动态规划

2.1 思路

2.2 代码实现

3. 方法二

3.1 思路

3.2 代码实现


1. 题目描述

给你一个下标从 0 开始的字符串 street 。street 中每个字符要么是表示房屋的 'H' ,要么是表示空位的 '.' 。

你可以在 空位 放置水桶,从相邻的房屋收集雨水。位置在 i - 1 或者 i + 1 的水桶可以收集位置为 i 处房屋的雨水。一个水桶如果相邻两个位置都有房屋,那么它可以收集 两个 房屋的雨水。

在确保 每个 房屋旁边都 至少 有一个水桶的前提下,请你返回需要的 最少 水桶数。如果无解请返回 -1 。

示例 1:

输入:street = "H..H"
输出:2
解释:
我们可以在下标为 1 和 2 处放水桶。
"H..H" -> "HBBH"('B' 表示放置水桶)。
下标为 0 处的房屋右边有水桶,下标为 3 处的房屋左边有水桶。
所以每个房屋旁边都至少有一个水桶收集雨水。

示例 2:

输入:street = ".H.H."
输出:1
解释:
我们可以在下标为 2 处放置一个水桶。
".H.H." -> ".HBH."('B' 表示放置水桶)。
下标为 1 处的房屋右边有水桶,下标为 3 处的房屋左边有水桶。
所以每个房屋旁边都至少有一个水桶收集雨水。

示例 3:

输入:street = ".HHH."
输出:-1
解释:
没有空位可以放置水桶收集下标为 2 处的雨水。
所以没有办法收集所有房屋的雨水。

示例 4:

输入:street = "H"
输出:-1
解释:
没有空位放置水桶。
所以没有办法收集所有房屋的雨水。

示例 5:

输入:street = "."
输出:0
解释:
没有房屋需要收集雨水。
所以需要 0 个水桶。

提示:

  • 1 <= street.length <= 10^5
  • street[i] 要么是 'H' ,要么是 '.' 。

2. 方法一:动态规划

2.1 思路

        针对每个房屋,有以下4种情况可以考虑:

  1. 两边都没有空位,包括位于两端的房屋其某一侧没有位置
  2. 左侧或者右侧有一个空位
  3. 两侧均有

        case1直接导致失败。

        case2使得该房屋没有选择,必须在其空位一侧安排一个水桶

        case3可选则在任何一边放置水桶,具体在哪边放置取决于放哪边能导致总的水桶数最少。

        第一感是考虑动态规划(递归方式)。

        考虑从左到右进行遍历。即dp(k)表示从位置索引k开始街道配置(即street[k:])所需要的最少水桶数。以下分类讨论。但是在递推的过程中,需要考虑H的隔壁的空位有可能已经放入了水桶(B)的情况,所以street在过程中会存在已经将“.”变为“B”的情况。

        以下分类讨论。

  •         (1)如果是从索引k往后第一个H(记其索引为j),左边是一个B而右边不能放置水桶(没有或者是H),则水桶数等于dp(j+1)
  •         (2)如果是从索引k往后第一个H(记其索引为j),左边是一个“.”而右边不能放置水桶,则该空位需要放一个水桶,水桶数等于1+dp(j+1)
  •         (3)如果是从索引k往后第一个H(记其索引为j),右边是一个“.”而左边不能放置水桶(没有或者是H),则必须在右边放置一个水桶,因此水桶数等于1+dp(j+1),但是street[j+1]要被修改为B
  •         (4)如果是从索引k往后第一个H(记其索引为j),右边是一个B而左边不能放置水桶。。。由于是从左向右遍历递推,所以这种情况不存在
  •         (5)如果是从索引k往后第一个H(记其索引为j),左边是一个B而右边也可以放置水桶,则水桶数与dp(j+1)相同,可以与case(1)合并
  •         (6)如果是从索引k往后第一个H(记其索引为j),左边是一个“.”而右边也可以放置水桶,则水桶数与dp(j+1)相同,进一步分为两种情况:
  •         (6-1)在左边放一个水桶,水桶数等于1+dp(j+1)
  •         (6-2)左边不放,右边放一个水桶,水桶数等于1+dp(j+1),但是street[j+1]要被修改为B

        从以上讨论可知,在dp(k)递归调用时,存在两种可能的情况,第1种对应于street[k]保持原值,第2中对应与street[k](原来是‘.’)但是被修改为B了。为了方便使用memoization技巧,考虑将递归函数定义为dp(k, flag),flag为0时表示street[k]保持原始值,flag为1时表示street[k]被置为B了。

        事实上,这个是不需要的,以上递推关系还可以优化(参见方法二)。但是这里保留以上的描述,忠实地记录思考过程。

2.2 代码实现

        (略)

3. 方法二

        可以考虑在方法一的基础上进行改进。

3.1 思路

        考虑先做一轮扫描,检查是否有无法放置水桶的H,如果有则直接返回False;如果没有的话,先对只有一侧可以放置水桶的H填充相应的水桶。

        经过以上第一轮遍历和修改后,如果street没有被判定为无解的话,street已经被修改为包含了“.”,“B”和“H”的序列,其中的H要么已经在其一侧已经有B了,要么两侧均为“.”。

        在此基础上再考虑递推关系,只需要考虑两侧均为“.”的H的处理,应该比方法一要更容易处理一些。

        如果是从索引k往后第一个两侧为“.”的H(记其索引为j),考虑以下两种情况(取二者之中返回结果较小者):

  • (1) 在其左边放置一个B,则水桶数等于1+dp(j+1)
  • (2) 在其右边放置一个B,则水桶数等于1+dp(j+1),但是street[j+1]被修改为B了

        事实上,在以上case(2)中,考虑street[j]可能有两种情况,如下所示:

  • (2-1) "***.H.H***" ==> "***.HBH***",接下去可以直接跳到dp(j+3)了
  • (2-2) "***.H..***" ==> "***.HB.***",接下去可以直接跳到dp(j+2)了
  • (2-2) "***.H.B***" ==> "***.HBB***",接下去可以直接跳到dp(j+2)了

        这样,就免掉了要修改street的麻烦了(当然请记住,此时的street已经是在第一轮更新后的street了)。

3.2 代码实现

import random
class Solution:
    def minimumBuckets(self, street: str) -> int:
        B_cnt = 0
        # The first round
        if street[0]=='H':
            if len(street)==1:
                return -1
            elif street[1]=='H':
                return -1
            else:
                street = 'HB'+street[2:]
                B_cnt += 1
        if street[-1]=='H':
            if street[-2]=='H':
                return -1
            elif street[-2]=='.': # it may already set to 'B'
                street = street[:-2]+'BH'
                B_cnt += 1
        
        for k in range(1,len(street)-1):
            if street[k-1]=='H' and street[k]=='H' and street[k+1]=='H':
                return -1
            elif street[k-1]=='.' and street[k]=='H' and street[k+1]=='H': # it may already set to 'B'
                street = street[:k-1] + 'B' + street[k:]
                B_cnt += 1
            elif street[k-1]=='H' and street[k]=='H' and street[k+1]=='.': # it may already set to 'B'
                # street[k+1] = 'B'
                street = street[:k+1] + 'B' + street[k+2:]
                B_cnt += 1
        # print(street,B_cnt)    
        # The second round
        memo = dict()   
        def dp(k) -> int:
            # print('dp: ',k)
            if k in memo:
                return memo[k]            
            # baseline case
            if k >= len(street)-2:
                return 0
            j = k+1  # instead of k!             
            # search for the first H with '.' in both side
            while j<=len(street)-2:
                if street[j]=='H' and street[j-1]=='.' and street[j+1]=='.':
                    ret1 = 1+dp(j+1)
                    if j==len(street)-2:
                        ret2 = 1
                    else:
                        if street[j+2]=='.' or street[j+2]=='B':                        
                            ret2 = 1+dp(j+2)
                        elif street[j+2]=='H':
                            ret2 = 1+dp(j+3)
                    return min(ret1,ret2)
                j += 1
            return 0    
            
        return B_cnt + dp(0)
if __name__ == '__main__':
    
    sln = Solution()
    
    street = "H..H"
    print('street={0}, ans={1}'.format(street,sln.minimumBuckets(street)))
    
    street = ".H.H."
    print('street={0}, ans={1}'.format(street,sln.minimumBuckets(street)))
    
    street = ".HHH."
    print('street={0}, ans={1}'.format(street,sln.minimumBuckets(street)))
    
    street = "H"
    print('street={0}, ans={1}'.format(street,sln.minimumBuckets(street)))
    
    street = "."
    print('street={0}, ans={1}'.format(street,sln.minimumBuckets(street)))

    street = ".."
    print('street={0}, ans={1}'.format(street,sln.minimumBuckets(street)))  
    
    street = "H."
    print('street={0}, ans={1}'.format(street,sln.minimumBuckets(street)))    
    
    street = "H.H"
    print('street={0}, ans={1}'.format(street,sln.minimumBuckets(street)))   
    
    street = ".HH.HH.HH.HH..H"
    print('street={0}, ans={1}'.format(street,sln.minimumBuckets(street)))
    
    # Random Test
    for i in range(1000):
        n = random.randint(1,50)
        street = ''
        for l in range(n):
            m = random.randint(0,3)
            street = street + ('H' if m==0 else '.')
        print('n={0}, street={1}, '.format(n,street), end='')
        ans = sln.minimumBuckets(street)
        print('ans={0}'.format(ans))

        费了九牛二虎之力终于实现好了(主要是各种边界条件很麻烦,还特意设计了一段随机测试代码,来测试各种情况),结果终于还是“一顿操作猛如虎,一看分数二百五”,leetcode提交结果超时。看来,方向错了就不免要事倍功半。

        不过嘛,至少这是我自主的解决方案,记录于此做个纪念。

        查看了一下官解(摘录如下),意外地很简单,是我想多了。。。惭愧ing。(代码后面再补,总不好意思看了解法思路还抄代码就不太像话了)

        方法一:贪心

        我们可以对字符串 street 从左到右进行一次遍历。每当我们遍历到一个房屋时,我们可以有如下的选择:

  1. 如果房屋的两侧已经有水桶,那么我们无需再放置水桶了;
  2. 如果房屋的两侧没有水桶,那么我们优先在房屋的「右侧」放置水桶,这是因为我们是从左到右进行遍历的,即当我们遍历到第 i 个位置时,前 i−1 个位置的房屋周围都是有水桶的。因此我们在左侧放置水桶没有任何意义,而在右侧放置水桶可以让之后的房屋使用该水桶。如果房屋的右侧无法放置水桶(例如是另一栋房屋或者边界),那么我们只能在左侧放置水桶。如果左侧也不能放置,说明无解。

        我们可以通过修改字符串来表示水桶的放置,从而实现上述算法。一种无需修改字符串的方法是,每当我们在房屋的右侧放置水桶时,可以直接「跳过」后续的两个位置,因为如果字符串形如 H.H,我们在第一栋房屋的右侧(即两栋房屋的中间)放置水桶后,就无需考虑第二栋房屋;如果字符串形如 H..,后续没有房屋,我们也可以全部跳过。

        回到主目录:笨牛慢耕的Leetcode解题笔记(动态更新。。。)

举报

相关推荐

0 条评论