目录
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种情况可以考虑:
- 两边都没有空位,包括位于两端的房屋其某一侧没有位置
- 左侧或者右侧有一个空位
- 两侧均有
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 从左到右进行一次遍历。每当我们遍历到一个房屋时,我们可以有如下的选择:
- 如果房屋的两侧已经有水桶,那么我们无需再放置水桶了;
- 如果房屋的两侧没有水桶,那么我们优先在房屋的「右侧」放置水桶,这是因为我们是从左到右进行遍历的,即当我们遍历到第 i 个位置时,前 i−1 个位置的房屋周围都是有水桶的。因此我们在左侧放置水桶没有任何意义,而在右侧放置水桶可以让之后的房屋使用该水桶。如果房屋的右侧无法放置水桶(例如是另一栋房屋或者边界),那么我们只能在左侧放置水桶。如果左侧也不能放置,说明无解。
我们可以通过修改字符串来表示水桶的放置,从而实现上述算法。一种无需修改字符串的方法是,每当我们在房屋的右侧放置水桶时,可以直接「跳过」后续的两个位置,因为如果字符串形如 H.H,我们在第一栋房屋的右侧(即两栋房屋的中间)放置水桶后,就无需考虑第二栋房屋;如果字符串形如 H..,后续没有房屋,我们也可以全部跳过。
回到主目录:笨牛慢耕的Leetcode解题笔记(动态更新。。。)