[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(nlogn)的贪婪算法:
首先采用一个复杂性为O(nlogn)的排序算法(如堆排序)按Si的递增次序排列各个任务,然后使用一个关于旧机器可用时间的最小堆。
例1-6[最短路径]给出一个有向网络,路径的长度定义为路径所经过的各边的耗费之和。
要求找一条从初始顶点s到达目的顶点d的最短路径。
贪婪算法分步构造这条路径,每一步在路径中加入一个顶点。
假设当前路径已到达顶点q,
且顶点q并不是目的顶点d。
加入下一个顶点所采用的贪婪准则为:
选择离q最近且目前不在路径中的顶点。
这种贪婪算法并不一定能获得最短路径。
例如,假设在图13-2中希望构造从顶点1到顶点5的最短路径,利用上述贪婪算法,从顶点1开始并寻找目前不在路径中的离顶点1最近的顶点。
到达顶点3,长度仅为2个单位,从顶点3可以到达的最近顶点为4,从顶点4到达顶点2,最后到达目的顶点5。
所建立的路径为1,3,4,2,5,其长度为10。
这条路径并不是有向图中从1到5的最短路径。
事实上,有几条更短的路径存在,例如路径1,4,5的长度为6。
根据上面三个例子,回想一下前几章所考察的一些应用,其中有几种算法也是贪婪算法。
例如,霍夫曼树算法,利用n-1步来建立最小加权外部路径的二叉树,每一步都将两棵二叉树合并为一棵,算法中所使用的贪婪准则为:
从可用的二叉树中选出权重最小的两棵。
LPT调度规则也是一种贪婪算法,它用n步来调度n个作业。
首先将作业按时间长短排序,然后在每一步中为一个任务分配一台机器。
选择机器所利用的贪婪准则为:
使目前的调度时间最短。
将新作业调度到最先完成的机器上(即最先空闲的机器)。
注意到在机器调度问题中,贪婪算法并不能保证最优,然而,那是一种直觉的倾向且一般情况下结果总是非常接近最优值。
它利用的规则就是在实际环境中希望人工机器调度所采用的规则。
算法并不保证得到最优结果,但通常所得结果与最优解相差无几,这种算法也称为启发式方法(heuristics)。
因此LPT方法是一种启发式机器调度方法。
定理9-2陈述了LPT调度的完成时间与最佳调度的完成时间之间的关系,因此LPT启发式方法具有限定性
能(boundedperformance)。
具有限定性能的启发式方法称为近似算法(approximationalgorithm)。
本章的其余部分将介绍几种贪婪算法的应用。
在有些应用中,贪婪算法所产生的结果总是最优的解决方案。
但对其他一些应用,生成的算法只是一种启发式方法,可能是也可能不是近似算法。
练习
1.证明找零钱问题(例13-4)的贪婪算法总能产生具有最少硬币数的零钱。
2.考虑例13-4的找零钱问题,假设售货员只有有限的25美分,10美分,5美分和1美分的硬币,给出一种找零钱的贪婪算法。
这种方法总能找出具有最少硬币数的零钱吗?
证明结论。
3.扩充例13-4的算法,假定售货员除硬币外还有50,20,10,5,和1美元的纸币,顾客买价格为x美元和y美分的商品时所付的款为u美元和v美分。
算法总能找出具有最少纸币与硬币数目的零钱吗?
证明结论。
4.编写一个C++程序实现例13-4的找零钱算法。
假设售货员具有面值为100,20,10,5和1美元的纸币和各种硬币。
程序可包括输入模块(即输入所买商品的价格及顾客所付的钱数),输出模块(输出零钱的数目及要找的各种货币的数目)和计算模块(计算怎样给出零钱)。
5.假设某个国家所使用硬币的币值为14,2,5和1分,则例13-4的贪婪算法总能产生具有最少硬币数的零钱吗?
证明结论。
6.1)证明例13-5的贪婪算法总能找到最优任务分配方案。
2)实现这种算法,使其复杂性为O(nlogn)(提示:
根据完成时间建立最小堆)。
*7.考察例13-5的机器调度问题。
假定仅有一台机器可用,那么将选择最大数量的任务在这台机器上执行。
例如,所选择的最大任务集合为{a,b,e}。
解决这种任务选择问题的贪婪算法可按步骤选择任务,每步选择一个任务,其贪婪准则如下:
从剩下的任务中选择具有最小的完成时间且不会与现有任务重叠的任务。
1)证明上述贪婪算法能够获得最优选择。
2)实现该算法,其复杂性应为O(nlogn)。
(提示:
采用一个完成时间的最小堆)
1.3应用
1.3.1货箱装船
这个问题来自例1 -2。
船可以分步装载,每步装一个货箱,且需要考虑装载哪一个货箱。
根据这种思想可利用如下贪婪准则:
从剩下的货箱中,选择重量最小的货箱。
这种选择次序可以保证所选的货箱总重量最小,从而可以装载更多的货箱。
根据这种贪婪策略,首先选择最轻的货箱,然后选次轻的货箱,如此下去直到所有货箱均装上船或船上不能再容纳其他任何一个货箱。
例1-7假设n=8,[w1,...w8]=[100,200,50,90,150,50,20,80],c=400。
利用贪婪算法时,所考察货箱的顺序为7,3,6,8,4,1,5,2。
货箱7,3,6,8,4,1的总重量为390个单位且已被装载,剩下的装载能力为10个单位,小于剩下的任何一个货箱。
在这种贪婪解决算法中得到[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++代码实现见程序13-1。
由于贪婪算法按货箱重量递增的顺序装载,程序13-1首先利用间接寻址排序函数IndirectSort对货箱重量进行排序(见3.5节间接寻址的定义),随后货箱便可按重量递增的顺序装载。
由于间接寻址排序所需的时间为O(nlogn)(也可利用9.5.1节的堆排序及第2章的归并排序),算法其余部分所需时间为O(n),因此程序13-1的总的复杂性为O(nlogn)。
程序13-1货箱装船
template
voidContainerLoading(intx[],Tw[],Tc,intn)
{//货箱装船问题的贪婪算法
//x[i]=1当且仅当货箱i被装载,1<=i<=n
//c是船的容量,w是货箱的重量
//对重量按间接寻址方式排序
//t是间接寻址表
int*t=newint[n+1];
IndirectSort(w,t,n);
//此时,w[t[i]]<=w[t[i+1]],1<=i//初始化x
for(inti=1;i<=n;i++)
x[i]=0;
//按重量次序选择物品
for(i=1;i<=n&&w[t[i]]<=c;i++){
x[t[i]]=1;
c-=w[t[i]];}//剩余容量
delete[]t;
}
1.3.20/1背包问题
在0/1背包问题中,需对容量为c的背包进行装载。
从n个物品中选取装入背包的物品,每件物品i的重量为wi,价值为pi。
对于可行的背包装载,背包中物品的总重量不能超过背包的容量,最佳装载是指所装入的物品价值最高,即nåi=1pixi取得最大值。
约束条件为nåi=1wixi≤c和xiÎ[0,1](1≤i≤n)。
在这个表达式中,需求出xt的值。
xi=1表示物品i装入背包中,xi=0表示物品i不装入背包。
0/1背包问题是一个一般化的货箱装载问题,即每个货箱所获得的价值不同。
货箱装载问题转化为背包问题的形式为:
船作为背包,货箱作为可装入背包的物品。
例1-8在杂货店比赛中你获得了第一名,奖品是一车免费杂货。
店中有n种不同的货物。
规则规定从每种货物中最多只能拿一件,车子的容量为c,物品i需占用wi的空间,价值为pi。
你的目标是使车中装载的物品价值最大。
当然,所装货物不能超过车的容量,且同一种物品不得拿走多件。
这个问题可仿照0/1背包问题进行建模,其中车对应于背包,货物对应于物品。
0/1背包问题有好几种贪婪策略,每个贪婪策略都采用多步过程来完成背包的装入。
在每一步过程中利用贪婪准则选择一个物品装入背包。
一种贪婪准则为:
从剩余的物品中,选出可以装入背包的价值最大的物品,利用这种规则,价值最大的物品首先被装入(假设有足够容量),然后是下一个价值最大的物品,如此继续下去。
这种策略不能保证得到最优解。
例如,考虑n=2,w=[100,10,10],p=[20,15,15],c=105。
当利用价值贪婪准则时,获得的解为x=[1,0,0],这种方案的总价值为20。
而最优解为[0,1,1],其总价值为30。
另一种方案是重量贪婪准则是:
从剩下的物品中选择可装入背包的重量最小的物品。
虽然这种规则对于前面的例子能产生最优解,但在一般情况下则不一定能得到最优解。
考虑n=2,w=[10,20],p=[5,100],c=25。
当利用重量贪婪策略时,获得的解为x=[1,0],比最优解[0,1]要差。
还可以利用另一方案,价值密度pi/wi贪婪算法,这种选择准则为:
从剩余物品中选择可
装入包的pi/wi值最大的物品,这种策略也不能保证得到最优解。
利用此策略试解n=3,w=[20,15,15],p=[40,25,25],c=30时的最优解。
我们不必因所考察的几个贪婪算法都不能保证得到最优解而沮丧,0/1背包问题是一个NP-复杂问题。
对于这类问题,也许根本就不可能找到具有多项式时间的算法。
虽然按pi/wi非递(增)减的次序装入物品不能保证得到最优解,但它是一个直觉上近似的解。
我们希望它是一个好的启发式算法,且大多数时候能很好地接近最后算法。
在600个随机产生的背包问题中,用这种启发式贪婪算法来解有239题为最优解。
有583个例子与最优解相差10%,所有600个答案与最优解之差全在25%以内。
该算法能在O(nlogn)时间内获得如此好的性能。
我们也许会问,是否存在一个x(x<100),使得贪婪启发法的结果与最优值相差在x%以内。
答案是否定的。
为说明这一点,考虑例子n=2,w=[1,y],p=[10,9y],和c=y。
贪婪算法结果为x=[1,0],这种方案的值为10。
对于y≥10/9,最优解的值为9y。
因此,贪婪算法的值与最优解的差对最优解的比例为((9y-10)/9y*100)%,对于大的y,这个值趋近于100%。
但是可以建立贪婪启发式方法来提供解,使解的结果与最优解的值之差在最优值的x%(x<100)之内。
首先将最多k件物品放入背包,如果这k件物品重量大于c,则放弃它。
否则,剩余的容量用来考虑将剩余物品按pi/wi递减的顺序装入。
通过考虑由启发法产生的解法中最多为k件物品的所有可能的子集来得到最优解。
例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]。
此解获得的价值为16。
现在考虑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],获得的价值为18。
若从子集{4}开始,产生的解为x=[1,0,0,1],获得的价值为19。
考虑子集大小为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],这些结果中最后一个价值为23,它的值比k=0和k=1时获得的解要高,这个答案即为启发式方法产生的结果。
这种修改后的贪婪启发方法称为k阶优化方法(k-optimal)。
也就是,若从答案中取出k件物品,并放入另外k件,获得的结果不会比原来的好,而且用这种方式获得的值在最优值的(100/(k+1))%以内。
当k=1时,保证最终结果在最佳值的50%以内;当k=2时,则在33.33%以内等等,这种启发式方法的执行时间随k的增大而增加,需要测试的子集数目为O(nk),每一个子集所需时间为O(n),因此当k>0时总的时间开销为O(nk+1)。
实际观察到的性能要好得多。
1.3.3拓扑排序
一个复杂的工程通常可以分解成一组小任务的集合,完成这些小任务意味着整个工程的完成。
例如,汽车装配工程可分解为以下任务:
将底盘放上装配线,装轴,将座位装在底盘上,上漆,装刹车,装门等等。
任务之间具有先后关系,例如在装轴之前必须先将底板放上装配线。
任务的先后顺序可用有向图表示