49.贪心算法之五:花园浇水问题
Posted 和孩子一起学Python
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了49.贪心算法之五:花园浇水问题相关的知识,希望对你有一定的参考价值。
由浅入深,3种方法介绍贪心算法的经典问题:花园浇水问题。
花园浇水问题
有n个花盆排成一排,花盆2侧有一个喷水龙头,也即n+1个龙头。给定每个龙头的喷水范围,问最少开多少个龙头,可以浇灌到每盆花。
比如下图中,有4盆花,5个水龙头。水龙头的喷水范围记录为 [2,3,0,0,1]。其中列表里的第1个元素2就表示第1个水龙头的喷水范围,它可以左边喷2格的距离,右边也是2格,当然,由于左边没有花盆,所以它实际上可以浇灌到它右边的2盆花。
花园浇水问题的本质其实是一个经典的算法模型。即
给定一个一维区间[0,n],以及n+1个小区间,问最少选择多少个小区间,就可以覆盖整个区间。
如果扩展到2维或者3维平台,难度将会更大。
如果还记得《46.贪心算法之二:做个好秘书,会议安排问题》的话,会发现两个问题有相通之处。
会议室安排问题的本质是
给定一个一维区间[0,n],以及m个不超出[0,n]范围的小区间,问如何选择最多个不相交的小区间。
会议室安排问题非常适用于贪心算法,每一步都选择
结束时间最早,且与上一个会议不冲突的会议。
假设给定的一维列表是[1,2,1,0,2,1,0,1],也即有8个水龙头。
如下图,我们把每个水龙头的浇灌范围画出来。
比如第1个水龙头,位置是0,值是1,浇灌范围是[0-1,0+1],也即[-1,1],因为负数没有意义,所以它有意义的浇灌范围是[0,1]。
第2个水龙头,位置是1,值是2,浇灌范围是[1-2,1+2],也即[-1,3],也去掉负数部分,有意义的浇灌范围是[0,3]。
第3个水龙头,位置是2,值是1,浇灌范围是[2-1,2+1],也即[1,3]。
……
在选择水龙头时,第1步,按照思维习惯,我们肯定要选择可以浇灌到第1盆花的水龙头。也即包含0的区间。
如上图,符合这个条件的子区间有[0,1]和[0,3],在这2个里,你会选择哪一个呢?根据贪心的思想,肯定选择[0,3]
也即,第1步你会选择一个 起点小于或者等于0,且终点尽可能大的区间。
当第1步选择了[0,3]之后,第2步要如何选择?
是不是选择 起点小于等于3,终点大于3,且尽可能大的区间?
为什么起点要小于等于3:因为第2个区间必须和第一个区间相加,比如第一个区间选择[0,3],第2个区间选择[4,8],中间就空了。
为什么终点要大于3:因为小于等于3就没有意义了。比如第一个区间选择[0,3],第2个区间选择[1,3],这是没有意义的。
有了上面的分析之后,程序就简单了。
首先用一个新的列表lands,把每个龙头覆盖的区间保存起来。
接下来就是2层循环。
外层循环每执行一轮,都选出1个龙头。
内层循环就是从所有的龙头里选出本轮符合条件的龙头。
def minTaps1(n, ranges) :
lands = []
for i in range(n+1):
lands.append([max(i-ranges[i],0),min(ranges[i]+i,n)])
minV = 0
maxV = 0
step = 0
for i in range(n+1):
temp = maxV
for j in range(n+1):
if lands[j][0] <= minV and lands[j][1] > maxV:
maxV = lands[j][1]
if maxV == n:
step += 1
break
elif maxV == temp:
step = -1
break
else:
step += 1
minV = maxV
return step
如下图中,每个水龙头上多了一个蓝色的数字。
它表示,能浇灌到这个位置的水龙头,最远能够浇灌到的位置。
这句话要怎么理解?
以第1个位置0为例,可以浇灌到这个位置的水龙头有2个,它们的浇灌区间分别是
[0,1],[0,3],
所以,在所有包含0这个位置的区间,最远的终点是3。
再比如1这个位置,可以浇灌到这个位置的水龙头有3个,它们的浇灌区间分别是
[0,1],[0,3],[1,3]
所以,在所有包含1这个位置的区间,最远的终点也是3。
再比如2这个位置,可以浇灌到这个位置的水龙头有3个,它们的浇灌区间分别是
[0,3],[1,3],[2,6]]
所以,在所有包含2这个位置的区间,最远的终点是6。
知道这点有什么好处呢?
第1步,按照思维习惯,我们肯定要选择可以浇灌到第1盆花的水龙头。也即包含0的区间。且终点尽可能大的区间。
而假设我们已经知道了包含每个位置的所有区间中,最远可以到达的位置,比如所有包含0这个位置的区间里,最远可以达到的位置是3。
所以,第1步,我们选择的区间最远可以达到3.
第2步,我们达到3这个位置,包含3的区间,最远可以达到6。所以第2步,我们选择的区间,最远可以达到6。
第3步,我们达到6这个位置,包含6的区间,最远可以达到7。所以第3步,我们选择的区间,最远可以达到7。也即一共选择3个区间,可以覆盖整个区间。
在整个过程中,要理解的是
前1个位置能达到的最远位置,后一个位置肯定也能达到。
比如0这个位置,因为有2个区间[0,1],[0,3]包含它,所以最远能达到的位置是3。
那么1这个位置,肯定至少也可以达到3。
程序如下,相对难理解的地方是,如何确定能浇灌到该位置的所有区间,最远可以达到的位置。
其实就是把所有的区间都循环一次,每次将这个区间包含的所有位置的最远位置都更新一次。
def minTaps2(n,ranges):
land = [0] * n
for i in range(n+1):
l = max(i - ranges[i],0)
r = min(i + ranges[i],n)
for j in range(l,r):
land[j] = max(land[j],r)
count = 0
cur = 0
while cur < n :
if land[cur] == 0 :
return -1
else:
cur = land[cur]
count += 1
return count
虽然第2种方法的时间复杂度要比第1种方法更优,但还是O(n*n)。
而接下来要介绍的这种方法,时间复杂度是O(n)。
如下图,每个水龙头的上面也多了一个数字。
怎么理解这个数字呢?
在下图中,用不同颜色的小柱子表示了不同的区间,我们可以想象成,这根小柱子是挂在它最右边的水龙头上的。
比如最下面那根绿色的柱子,可以想象它是挂在7那个水龙头上。
而6那个水龙头,则挂了3根小柱子,只不过其中一根的长度是0。
下图中每个水龙头上的数字,表示的是
挂在这个水龙头上的所有柱子,最左可以达到什么位置。
如果我们已经有了这些数字,然后从右往左找。
第1步:7位置上的水龙头,挂的所有柱子,最左只能达到6,所以它只能跳1步,达到6的位置。
第2步:6位置上的水龙头,挂的所有柱子,最左能到底2。
是不是从6直接跳到2呢?
不是!
我们比较(6,2]里的每一个水龙头,也即要比较假设分别以5、4、3、2 为“垫脚石”,再下一步谁跳得更远。
在下图中,6-->3-->0,而6-->2-->2,也即从6跳到3,再跳一步可 以跳到0,而从6跳到2,再跳一步还是只能到2。
至于怎么 得到挂在这个水龙头上的所有柱子,最左可以达到什么位置 ,只要从左到右变量一遍就可以了。
程序如下:
def minTaps3(n, ranges) :
prev = [x for x in range(n + 1)]
for i in range(n + 1):
l = max(i - ranges[i], 0)
r = min(i + ranges[i], n)
prev[r] = min(prev[r], l)
breakpoint, furthest = n, 2 ** 30
ans = 0
for i in range(n, 0, -1):
furthest = min(furthest, prev[i])
if i == breakpoint:
if furthest >= i:
return -1
breakpoint = furthest
ans += 1
return ans
假设有如下地图,一共有5个地点。
带箭头的线段表示可行的道路,注意,道路是单向的,如下图中可以从1去到2,但是不能从2去到1。
线段上的数字表示路程的长度。
问题是,从地点1去往其它地点的最短路径分别是多少?
扫描二维码
获取更多精彩
少儿编程
以上是关于49.贪心算法之五:花园浇水问题的主要内容,如果未能解决你的问题,请参考以下文章