贪婪算法
第 1 章 贪婪算法
虽然设计一个好的求解算法更像是一门艺术~而不像是技术~但仍然存在一些行之有效的能够用于解决许多问题的算法设计方法~你可以使用这些方法来设计算法~并观察这些算法是如何工作的。一般情况下~为了获得较好的性能~必须对算法进行细致的调整。但是在某些情况下~算法经过调整之后性能仍无法达到要求~这时就必须寻求另外的方法来求解该问题。
本章首先引入最优化的概念~然后介绍一种直观的问题求解方法:贪婪算法。最后~应用该算法给出货箱装船问题、背包问题、拓扑排序问题、二分覆盖问题、最短路径问题、最小代价生成树等问题的求解
。
1.1 最优化问题
本章及后续章节中的许多例子都是最优化问题, optimization problem,~每个最优化问题都包含一组限制条件, c o n s t r a i n t,和一个优化函数, optimization function,~符合限制条件的问题求解方案称为可行解, feasible solution,~使优化函数取得最佳值的可行解称为最优解,optimal solution,。
例1-1 [ 渴婴问题] 有一个非常渴的、聪明的小婴儿~她可能得到的东西包括一杯水、一桶牛奶、多罐不同种类的果汁、许多不同的装在瓶子或罐子中的苏打水~即婴儿可得到n 种不同的饮料。根据以前关于这n 种饮料的不同体验~此婴儿知道这其中某些饮料更合自己的胃口~因此~婴儿采取如下方法为每一种饮料赋予一个满意度值:饮用1盎司第i 种饮料~对它作出相对评价~将一个数值si 作为满意度赋予第i 种饮料。
通常~这个婴儿都会尽量饮用具有最大满意度值的饮料来最大限度地满足她解渴的需要~但是不幸的是:具有最大满意度值的饮料有时并没有足够的量来满足此婴儿解渴的需要。设ai是第i 种饮料的总量,以盎司为单位,~而此婴儿需要t 盎司的饮料来解渴~那么~需要饮用n种不同的饮料各多少量才能满足婴儿解渴的需求呢,
设各种饮料的满意度已知。令xi 为婴儿将要饮用的第i 种饮料的量~则需要解决的问题是: 找到一组实数xi,1?i?n,~使n ,i = 1si xi 最大~并满足:n ,i=1xi =t 及0?xi?ai 。
需要指出的是:如果n ,i = 1ai < t~则不可能找到问题的求解方案~因为即使喝光所有的饮料也不能使婴儿解渴。
对上述问题精确的数学描述明确地指出了程序必须完成的工作~根据这些数学公式~可以对输入/ 输出作如下形式的描述:
输入:n~t~si ~ai,其中1?i?n~n 为整数~t、si 、ai 为正实数,。
输出:实数xi,1?i?n,~使n ,i= 1si xi 最大且n ,i=1xi =t,0?xi?ai,。如果n ,i = 1ai
标准下,。决策一旦作出~就不可再更改。作出贪婪决策的依据称为贪婪准则,greedy criterion,。
例1-4 [找零钱] 一个小孩买了价值少于1美元的糖~并将1美元的钱交给售货员。售货员希望用数目最少的硬币找给小孩。假设提供了数目不限的面值为2 5美分、1 0美分、5美分、及1美分的硬币。售货员分步骤组成要找的零钱数~每次加入一个硬币。选择硬币时所采用的贪婪准则如下:每一次选择应使零钱数尽量增大。为保证解法的可行性,即:所给的零钱等于要找的零钱数,~所选择的硬币不应使零钱总数超过最终所需的数目。
假设需要找给小孩6 7美分~首先入选的是两枚2 5美分的硬币~第三枚入选的不能是2 5美分的硬币~否则硬币的选择将不可行,零钱总数超过6 7美分,~第三枚应选择1 0美分的硬币~然后是5美分的~最后加入两个1美分的硬币。
贪婪算法有种直觉的倾向~在找零钱时~直觉告诉我们应使找出的硬币数目最少,至少是接近最少的数目,。可以证明采用上述贪婪算法找零钱时所用的硬币数目的确最少,见练习1,。 例1-5 [机器调度] 现有n 件任务和无限多台的机器~任务可以在机器上得到处理。每件任务的开始时间为si~完成时间为fi ~si < fi 。[si , fi ] 为处理任务i 的时间范围。两个任务i~j 重指两个任务的时间范围区间有重叠~而并非是指i~j 的起点或终点重合。例如:区间[ 1~4 ]与区间[ 2~4 ]重叠~而与区间[ 4~7 ]不重叠。一个可行的任务分配是指在分配中没有两件重叠的任务分配给同一台机器。因此~在可行的分配中每台机器在任何时刻最多只处理一个任务。最优分配是指使用的机器最少的可行分配方案。
假设有n= 7件任务~标号为a 到g。它们的开始与完成时间如图13-1a 所示。若将任务a分给机器M1~任务b 分给机器M2~. . .~任务g 分给机器M7~这种分配是可行的分配~共使用了七台机器。但它不是最优分配~因为有其他分配方案可使利用的机器数目更少~例如:可以将任务a、b、d分配给同一台机器~则机器的数目降为五台。
一种获得最优分配的贪婪方法是逐步分配任务。每步分配一件任务~且按任务开始时间的非递减次序进行分配。若已经至少有一件任务分配给某台机器~则称这台机器是旧的,若机器非旧~则它是新的。在选择机器时~采用以下贪婪准则:根据欲分配任务的开始时间~若此时有旧的机器可用~则将任务分给旧的机器。否则~将任务分配给一台新的机器。
根据例子中的数据~贪婪算法共分为n = 7步~任务分配的顺序为a、f、b、c、g、e、d。第一步没有旧机器~因此将a 分配给一台新机器,比如M1,。这台机器在0到2时刻处于忙状态。在第二步~考虑任务f。由于当f 启动时旧机器仍处于忙状态~因此将f 分配给一台新机器(设为M2 )。第三步考虑任务b, 由于旧机器M1在Sb = 3时刻已处于闲状态~因此将b分配给M1执行~M1下一次可用时刻变成fb = 7~M2的可用时刻变成ff = 5。第四步~考虑任务c。由于没有旧机器在Sc = 4时刻可用~因此将c 分配给一台新机器,M3,~这台机器下一次可用时间为fc = 7。第五步考虑任务g~将其分配给机器M2~第六步将任务e 分配给机器M1, 最后在第七步~任务
2分配给机器M3。,注意:任务d 也可分配给机器M2,。
上述贪婪算法能导致最优机器分配的证明留作练习,练习7,。可按如下方式实现一个复杂性为O (nl o gn)的贪婪算法:首先采用一个复杂性为O (nl o gn)的排序算法,如堆排序,按Si 的递增次序排列各个任务~然后使用一个关于旧机器可用时间的最小堆。
例1-6 [最短路径] 给出一个有向网络~路径的长度定义为路径所经过的各边的耗费之和。要求找一条从初始顶点s 到达目的顶点d 的最短路径。
贪婪算法分步构造这条路径~每一步在路径中加入一个顶点。假设当前路径已到达顶点q~ 且顶点q 并不是目的顶点d。加入下一个顶点所采用的贪婪准则为:选择离q 最近且目前不在路径中的顶点。
这种贪婪算法并不一定能获得最短路径。例如~假设在图1 3 - 2中希望构造从顶点1到顶点5的最短路径~利用上述贪婪算法~从顶点1开始并寻找目前不在路径中的离顶点1最近的顶点。到达顶点3~长度仅为2个单位~从顶点3可以到达的最近顶点为4~从顶点4到达顶点2~最后到达目的顶点5。所建立的路径为1 , 3 , 4 , 2 , 5~其长度为1 0。这条路径并不是有向图中从1到5的最短路径。事实上~有几条更短的路径存在~例如路径1~4~5的长度为6。
根据上面三个例子~回想一下前几章所考察的一些应用~其中有几种算法也是贪婪算法。例如~霍夫曼树算法~利用n- 1步来建立最小加权外部路径的二叉树~每一步都将两棵二叉树合并为一棵~算法中所使用的贪婪准则为:从可用的二叉树中选出权重最小的两棵。L P T调度规则也是一种贪婪算法~它用n 步来调度n 个作业。首先将作业按时间长短排序~然后在每一步中为一个任务分配一台机器。选择机器所利用的贪婪准则为:使目前的调度时间最短。将新作业调度到最先完成的机器上,即最先空闲的机器,。
注意到在机器调度问题中~贪婪算法并不能保证最优~然而~那是一种直觉的倾向且一般情况下结果总是非常接近最优值。它利用的规则就是在实际环境中希望人工机器调度所采用的规则。算法并不保证得到最优结果~但通常所得结果与最优解相差无几~这种算法也称为启发式方法, h e u r i s t i c s )。因此L P T方法是一种启发式机器调度方法。定理9 - 2陈述了L P T调度的完成时间与最佳调度的完成时间之间的关系~因此L P T启发式方法具有限定性 能, bounded performance ,。具有限定性能的启发式方法称为近似算法, a p p r o x i m a t i o na
l g o r i t h m,。
本章的其余部分将介绍几种贪婪算法的应用。在有些应用中~贪婪算法所产生的结果总是最优的解决方案。但对其他一些应用~生成的算法只是一种启发式方法~可能是也可能不是近似算法。
练习
1. 证明找零钱问题,例1 3 - 4,的贪婪算法总能产生具有最少硬币数的零钱。 2. 考虑例1 3 - 4的找零钱问题~假设售货员只有有限的2 5美分~ 1 0美分~ 5美分和1美分的硬币~给出一种找零钱的贪婪算法。这种方法总能找出具有最少硬币数的零钱吗,证明结论。 3. 扩充例1 3 - 4的算法~假定售货员除硬币外还有50, 20, 10, 5, 和1美元的纸币~顾客买价格为x 美元和y 美分的商品时所付的款为u 美元和v 美分。算法总能找出具有最少纸币与硬币数目的零钱吗,证明结论。
4. 编写一个C + +程序实现例1 3 - 4的找零钱算法。假设售货员具有面值为1 0 0~2 0~1 0~5和1美元的纸币和各种硬币。程序可包括输入模块,即输入所买商品的价格及顾客所付的钱数,~输出模块,输出零钱的数目及要找的各种货币的数目,和计算模块,计算怎样给出零钱,。 5. 假设某个国家所使用硬币的币值为1 4 , 2 , 5和1分~则例1 3 - 4的贪婪算法总能产生具有最少硬币数的零钱吗,证明结论。
6. 1) 证明例1 3 - 5的贪婪算法总能找到最优任务分配方案。
2) 实现这种算法~使其复杂性为O (nl o gn),提示:根据完成时间建立最小堆,。
*7. 考察例1 3 - 5的机器调度问题。假定仅有一台机器可用~那么将选择最大数量的任务在这台机器上执行。例如~所选择的最大任务集合为{a,b,e}。解决这种任务选择问题的贪婪算法可按步骤选择任务~每步选择一个任务~其贪婪准则如下:从剩下的任务中选择具有最小的完成时间且不会与现有任务重叠的任务。
1) 证明上述贪婪算法能够获得最优选择。
2) 实现该算法~其复杂性应为O(nl o gn)。,提示:采用一个完成时间的最小堆,
1.3 应用
1.3.1 货箱装船
这个问题来自例1 - 2。船可以分步装载~每步装一个货箱~且需要考虑装载哪一个货箱。根据这种思想可利用如下贪婪准则:从剩下的货箱中~选择重量最小的货箱。这种选择次序可以保证所选的货箱总重量最小~从而可以装载更多的货箱。根据这种贪婪策略~首先选择最轻的货箱~然后选次轻的货箱~如此下去直到所有货箱均装上船或船上不能再容纳其他任何一个货箱。
例1-7 假设n =8, [w1 , ... w8 ]=[100,200,50,90,150,50,20,80], c= 4 0 0。利用贪婪算法时~所考察货箱的顺序为7 , 3 , 6 , 8 , 4 , 1 , 5 , 2。货箱7 , 3 , 6 , 8 , 4 , 1的总重量为3 9 0个单位且已被装载~剩下的装载能力为1 0个单位~小于剩下的任何一个货箱。在这种贪婪解决算法中得到[x1 , ..., x8 ] = [ 1 , 0 , 1 , 1 , 0 , 1 , 1 , 1 ]且,xi = 6。
定理1-1 利用贪婪算法能产生最佳装载。
证明可以采用如下方式来证明贪婪算法的最优性:令x = [x1 , ..., xn ]为用贪婪算法获得的解~令y =[ y1 , ..., yn ]为任意一个可行解~只需证明n ,i= 1xi ?n ,i= 1yi 。不失一般性~可以假设货箱都排好了序:即wi?wi + 1,1?i?n,。然后分几步将y 转化为x~转换过程中每一步都产生一个可行的新y~且n ,i = 1yi 大于等于未转化前的值~最后便可证明n ,i = 1xi ?n ,j = 1yi 。
根据贪婪算法的工作过程~可知在[0, n] 的范围内有一个k~使得xi =1, i?k且xi =0, i>k。寻找[ 1 ,n]范围内最小的整数j~使得xj?yj 。若没有这样的j 存在~则n ,i= 1xi =n ,i = 1yi 。如果有这样的j 存在~则j?k~否则y 就不是一个可行解~因为xj?yj ~xj = 1且yj = 0。令yj = 1~若结果得到的y 不是可行解~则在[ j+ 1 ,n]范围内必有一个l 使得yl = 1。令yl = 0~由于wj?wl ~则得到的y 是可行的。而且~得到的新y 至少与原来的y 具有相同数目的1。
经过数次这种转化~可将y 转化为x。由于每次转化产生的新y 至少与前一个y 具有相同数目的1~因此x 至少与初始的y 具有相同的数目1。货箱装载算法的C + +代码实现见程序1 3 - 1。由于贪婪算法按货箱重量递增的顺序装载~程序1 3 - 1首先利用间接寻址排序函数I n d i r e c t S o r t对货箱重量进行排序,见3 . 5节间接寻址的定义,~随后货箱便可按重量递增的顺序装载。由于间接寻址排序所需的时间为O (nl o gn),也可利用9 . 5 . 1节的堆排序及第2章的归并排序,~算法其余部分所需时间为O (n)~因此程序1 3 - 1的总的复杂性为O (nl o gn)。 程序13-1 货箱装船
template
void ContainerLoading(int x[], T w[], T c, int n) {// 货箱装船问题的贪婪算法
// x[i] = 1 当且仅当货箱i被装载~ 1<=i<=n
// c是船的容量, w 是货箱的重量
// 对重量按间接寻址方式排序
// t 是间接寻址表
int *t = new int [n+1];
I n d i r e c t S o r t ( w, t, n);
// 此时, w[t[i]] <= w[t[i+1]], 1<=i规定 从每种货物中最多只能拿一件~车子的容量为c~物品i 需占用wi 的空间~价值为pi 。你的目标是使车中装载的物品价值最大。当然~所装货物不能超过车的容量~且同一种物品不得拿走多件。这个问题可仿照0 / 1背包问题进行建模~其中车对应于背包~货物对应于物品。
0 / 1背包问题有好几种贪婪策略~每个贪婪策略都采用多步过程来完成背包的装入。在每一步过程中利用贪婪准则选择一个物品装入背包。一种贪婪准则为:从剩余的物品中~选出可以装入背包的价值最大的物品~利用这种规则~价值最大的物品首先被装入,假设有足够容量,~然后是下一个价值最大的物品~如此继续下去。这种策略不能保证得到最优解。例如~考虑n=2, w=[100,10,10], p =[20,15,15], c = 1 0 5。当利用价值贪婪准则时~获得的解为x= [ 1 , 0 , 0 ]~这种方案的总价值为2 0。而最优解为[ 0 , 1 , 1 ]~其总价值为3 0。
另一种方案是重量贪婪准则是:从剩下的物品中选择可装入背包的重量最小的物品。虽然这种规则对于前面的例子能产生最优解~但在一般情况下则不一定能得到最优解。考虑n= 2 ,w=[10,20], p=[5,100], c= 2 5。当利用重量贪婪策略时~获得的解为x =[1,0], 比最优解[ 0 , 1 ]要差。
还可以利用另一方案~价值密度pi /wi 贪婪算法~这种选择准则为:从剩余物品中选择可 装入包的pi /wi 值最大的物品~这种策略也不能保证得到最优解。利用此策略试解n= 3 ,w=[20,15,15], p=[40,25,25], c=30 时的最优解。
我们不必因所考察的几个贪婪算法都不能保证得到最优解而沮丧~ 0 / 1背包问题是一个N P-复杂问题。对于这类问题~也许根本就不可能找到具有多项式时间的算法。虽然按pi /wi 非递,增,减的次序装入物品不能保证得到最优解~但它是一个直觉上近似的解。我们希望它是一个好的启发式算法~且大多数时候能很好地接近最后算法。在6 0 0个随机产生的背包问题中~用这种启发式贪婪算法来解有2 3 9题为最优解。有5 8 3个例子与最优解相差1 0 %~所有6 0 0个答案与最优解之差全在2 5 %以内。该算法能在O (nl o gn)时间内获得如此好的性能。我们也许会问~是否存在一个x (x<1 0 0 )~使得贪婪启发法的结果与最优值相差在x%以内。答案是否定的。为说明这一点~考虑例子n =2, w = [ 1 ,y], p= [ 1 0 , 9y], 和c= y。贪婪算法结果为x=[1,0], 这
种方案的值为1 0。对于y?1 0 / 9~最优解的值为9 y。因此~贪婪算法的值与最优解的差对最优解的比例为( ( 9y - 1 0)/9y* 1 0 0 ) %~对于大的y~这个值趋近于1 0 0 %。但是可以建立贪婪启发式方法来提供解~使解的结果与最优解的值之差在最优值的x% (x<100) 之内。首先将最多k 件物品放入背包~如果这k 件物品重量大于c~则放弃它。否则~剩余的容量用来考虑将剩余
i /wi 递减的顺序装入。通过考虑由启发法产生的解法中最多为k 件物品的所有可能的子物品按p
集来得到最优解。
例13-9 考虑n =4, w=[2,4,6,7], p=[6,10,12,13], c = 11。当k= 0时~背包按物品价值密度非递减顺序装入~首先将物品1放入背包~然后是物品2~背包剩下的容量为5个单元~剩下的物品没有一个合适的~因此解为x = [ 1 , 1 , 0 , 0 ]。此解获得的价值为1 6。
现在考虑k = 1时的贪婪启发法。最初的子集为{ 1 } , { 2 } , { 3 } , { 4 }。子集{ 1 } , { 2 }产生与k= 0时相同的结果~考虑子集{ 3 }~臵x3 为1。此时还剩5个单位的容量~按价值密度非递增顺序来考虑如何利用这5个单位的容量。首先考虑物品1~它适合~因此取x1 为1~这时仅剩下3个单位容量了~且剩余物品没有能够加入背包中的物品。通过子集{ 3 }开始求解得结果为x = [ 1 , 0 , 1 , 0 ]~获得的价值为1 8。若从子集{ 4 }开始~产生的解为x = [ 1 , 0 , 0 , 1 ]~获得的价值为1 9。考虑子集大小为0和1时获得的最优解为[ 1 , 0 , 0 , 1 ]。这个解是通过k= 1的贪婪启发式算法得到的。
若k= 2~除了考虑k< 2的子集~还必需考虑子集{ 1 , 2 } , { 1 , 3 } , { 1 , 4 } , { 2 , 3 } , { 2 , 4 }
和{ 3 , 4 }。首先从最后一个子集开始~它是不可行的~故将其抛弃~剩下的子集经求解分别得到如下结果:[ 1 , 1 , 0 , 0 ] , [ 1 , 0 , 1 , 0 ] , [ 1 , 0 , 0 , 1 ] , [ 0 , 1 , 1 , 0 ]和[ 0 , 1 , 0 , 1 ]~这些结果中最后一个价值为2 3~它的值比k= 0和k= 1时获得的解要高~这个答案即为启发式方法产生的结果。
这种修改后的贪婪启发方法称为k阶优化方法,k - o p t i m a l,。也就是~若从答案中取出k 件物品~并放入另外k 件~获得的结果不会比原来的好~而且用这种方式获得的值在最优值的( 1 0 0 / (k + 1 ) ) %以内。当k= 1时~保证最终结果在最佳值的5 0 %以内,当k= 2时~则在3 3 . 3 3 %以内等等~这种启发式方法的执行时间随k 的增大而增加~需要测试的子集数目为O (nk )~每一个子集所需时间为O (n)~因此当k >0时总的时间开销为O (nk+1 )。实际观察到的性能要好得多。
1.3.3 拓扑排序
一个复杂的
通常可以分解成一组小任务的集合~完成这些小任务意味着整个工程的完成。例如~汽车装配工程可分解为以下任务:将底盘放上装配线~装轴~将座位装在底盘上~上漆~装刹车~装门等等。任务之间具有先后关系~例如在装轴之前必须先将底板放上装配线。任务的先后顺序可用有向图表示——称为顶点活动, Activity On Vertex, AOV,网络。有向图的顶点代表任务~有向边(i, j) 表示先后关系:任务j 开始前任务i 必须完成。图1 - 4显示了六个任务的工程~边, 1 , 4,表示任务1在任务4开始前完成~同样边, 4 , 6,表示任务4在任务6开始前完成~边,1 , 4,与,4 , 6,合起来可知任务1在任务6开始前完成~即前后关系是传递的。由此可知~边,1 , 4,是多余的~因为边,1 , 3,和,3 , 4,已暗示了这种关系。
在很多条件下~任务的执行是连续进行的~例如汽车装配问题或平时购买的标有“需要装配”的消费品,自行车、小孩的秋千装臵~割草机等等,。我们可根据所建议的顺序来装配。在由任务建立的有向图中~边, i, j,表示在装配序列中任务i 在任务j 的前面~具有这种性质的序列称为拓扑序列,topological orders或topological sequences)。根据任务的有向图建立拓扑序列的过程称为拓扑排序,topological sorting,。图1 - 4的任务有向图有多种拓扑序列~其中的三种为1 2 3 4 5 6~1 3 2 4 5 6和2 1 5 3 4 6~序列1 4 2 3 5 6就不是拓扑序列~因为在这个序列中任务4在3的前面~而任务有向图中的边为, 3 , 4,~这种序列与边, 3 , 4,及其他边所指示的序列
相矛盾。可用贪婪算法来建立拓扑序列。算法按从左到右的步骤构造拓扑序列~每一步在排好的序列中加入一个顶点。利用如下贪婪准则来选择顶点:从剩下的顶点中~选择顶点w~使得w 不存在这样的入边, v,w,~其中顶点v 不在已排好的序列结构中出现。注意到如果加入的顶点w违背了这个准则,即有向图中存在边, v,w,且v 不在已构造的序列中,~则无法完成拓扑排序~因为顶点v 必须跟随在顶点w 之后。贪婪算法的伪代码如图1 3 - 5所示。while 循环的每次迭代代表贪婪算法的一个步骤。
现在用贪婪算法来求解图1 - 4的有向图。首先从一个空序列V开始~第一步选择V的第一个顶点。此时~在有向图中有两个候选顶点1和2~若选择顶点2~则序列V = 2~第一步完成。第二步选择V的第二个顶点~根据贪婪准则可知候选顶点为1和5~若选择5~则V = 2 5。下一步~顶点1是唯一的候选~因此V = 2 5 1。第四步~顶点3是唯一的候选~因此把顶点3加入V 得到V = 2 5 1 3。在最后两步分别加入顶点4和6 ~得V = 2 5 1 3 4 6。
1. 贪婪算法的正确性
为保证贪婪算法算的正确性~需要证明: 1) 当算法失败时~有向图没有拓扑序列, 2) 若 算法没有失败~V即是拓扑序列。2) 即是用贪婪准则来选取下一个顶点的直接结果~ 1) 的证
j qj + 1.qk qj , 则明见定理1 3 - 2~它证明了若算法失败~则有向图中有环路。若有向图中包含环q它没有拓扑序列~因为该序列暗示了qj 一定要在qj 开始前完成。
定理1-2 如果图1 3 - 5算法失败~则有向图含有环路。
证明注意到当失败时| V |
记录 拓扑次序
// 如果不存在拓扑次序~则返回f a l s e
int n = Ve r t i c e s ( ) ; // 计算入度
int *InDegree = new int [n+1]; InitializePos(); // 图遍历器数组
for (int i = 1; i <= n; i++) // 初始化
InDegree[i] = 0;
for (i = 1; i <= n; i++) {// 从i 出发的边
int u = Begin(i);
while (u) {
I n D e g r e e [ u ] + + ; u = NextVe r t e x ( i ) ; } }
// 把入度为,的顶点压入堆栈
LinkedStack