第五章 贪心算法
§1 基本思想
找零钱 假如售货员需要找给小孩 67 美分的零钱。现在,售货员
手中只有 25 美分、 10 美分、 5 美分和 1 美分的硬币。在小孩的催促
下,售货员想尽快将钱找给小孩。她的做法是:先找不大于 67 美分
的最大硬币 25 美分硬币,再找不大于 67- 25= 42 美分的最大硬币
25 美分硬币,再找不大于 42- 25= 17 美分的最大硬币 10 美分硬币,
再找不大于 17- 10= 7 美分的最大硬币 5 美分硬币, 最后售货员再找
出两个 1 美分的硬币。至此,售货员共找给小孩 6 枚硬币。售货员的
原则是拿尽可能少的硬币个数找给小孩。从另一个角度看,如果售货
员将捡出的硬币逐一放在手中,最后一起交给小孩,那么售货员想使
自己手中的钱数增加的尽量快些, 所以每一次都尽可能地捡面额大的
硬币。
装载问题 有一艘大船用来装载货物。假设有 n 个货箱,它们的
体积相同,重量分别是
n
www ,,
21
L,货船的最大载重量是 c。目标
是在船上装最多货箱该怎样装?如果用 1=
i
x 表示装第 i 个货箱,而
0=
i
x 表示不装第 i 个货箱,则上述问题是解优化问题:求 x
1
, x
2
,??????,
x
n
,
满足 cx
i
n
i
i
≤
∑
=1
w (5.1)
使得
∑
=
n
i
i
x
1
max (5.2)
贪心方法,顾名思义,是在决策中 总是作出在当前看来是最好的
选择 。例如找零钱问题中,售货员每捡一个硬币都想着使自己手中的
钱尽快达到需要找钱的总数。在装载问题中,每装一个货箱都想着在
不超重的前提下让船装进更多的箱子。 但是贪心方法并未考虑整体最
优解,它所作出的选择只是在某种意 义上的局部最优选择。当然,在
采用贪心算法时未必不希望结果是整体最优的。事实上,有相当一部
分问题,采用贪心算法能够达到整体最优。如前面的找零钱问题以及
后面将要讲到的单点源最短路径问题、最小生成树问题、工件排序问
题等。为了更好理解贪心算法,我们 将装载问题稍加推广,考虑 0/1
背包问题。
0/1 背包问题 已知容量为 M 的背包和 n 件物品。第 i 件物品的
重量为 w
i
,价值是 p
i
。因而将物品 i 的一部分 x
i
放进背包即获得 p
i
x
i
的价值。问题是:怎样装包使所获得的价值最大。即是如下的优化问
题:
∑
≤≤ ni
ii
xp
1
max (5.3)
niwpx
Mxw
iii
ni
ii
≤≤>>≤≤
≤
∑
≤≤
1,0,0,10
1
(5.4)
采用贪心方法,有几种原则可循: a.每次捡最轻的物品装; b.每次捡
价值最大的装; c.每次装包时既考虑物品的重量又考虑物品的价值,
也就是说每次捡单位价值
ii
wp 最大的装。按原则 a 来装只考虑到多
装些物品,但由于单位价值未必高,总价值不能达到最大;按原则 b
来装,每次选择的价值最大,但同时也可能占用了较大的空间,装的
物品少,未变能够达到总价值最大。比较合理的原则是 c。事实上,
按照原则 c 来装,确实能够达到总价值最大。
0/1背包问题贪心算法
GreedyKnapsack(p, w, M, x, n) //价值数组 p[1:n]、重量数组 w[1:n],
//它们元素的排列顺序是 p[i]/w[i]≥ p[i+1]/w[i+1]; M是背包容量,
// x 是解向量
real p[1:n], w[1:n], x[1:n], M, rc;
integer i, n;
x=0; // 将解向量初始化为零
rc=M; // 是背包剩余容量初始化为 M
for i from 1 to n do
if w[i] > rc then exit endif
x[i]=1; rc=rc-w[i];
endfor
if i≤n then
x[i]=rc/w[i];
endif
end GreedyKnapsack
定理 1 如果 p[1]/w[2]≥p[2]/w[2]≥ ??????≥p[n]/w[n],则
GreedyKnapsack 对于给定的0/1 背包问题实例生成一个最优解。
证明 设x=(x
1
,x
2
,??????,x
n
)是GreedyKnapsack 所生成的解, 但不是
最优解。因而必有某个x
i
不为1。不妨设x
j
是第一个这样的分量。于
是,当1 ≤ i<j时,x
i
=1;当j<i ≤n 时,x
i
=0;当i=j 时,0 ≤x
i
<1。
因为 x 不是最优解,则存在解向量 y=(y
1
,y
2
,??????,y
n
),使得
∑∑
>
iiii
xpyp 。不妨假定 Mxw
ii
=
∑
。设 k 是使得 y
k
≠ x
k
的最小
下标, 则y
k
< x
k
. 这是因为:当k<j 时,x
k
=1,上述不等式自然成立;
当k ≥j 时,因为x
1
=y
1
, x
2
=y
2
,??????, x
k-1
= y
k-1
,当k<i ≤n时,x
i
=0,所
以由y
k
> x
k
可推出 Mxwyw
iiii
=>
∑∑
,y 不是解向量,矛盾。
现在取新的向量z=(z
1
,z
2
,??????,z
n
)满足
z
1
=y
1
, z
2
=y
2
,??????, z
k-1
= y
k-1
, z
k
= x
k
0≤z
k+1
≤y
k+1
, ??????, 0≤z
n
≤y
n
而且 )()(
1
kkk
nik
iii
yzwzyw ?=?
∑
≤≤+
这样的向量z 是存在的,而且是0/1 背包问题的解,因为
Myw
ywyw
zwzwywzw
ni
ii
nik
ii
ki
ii
nik
iikk
ki
ii
ni
ii
≤=
+=
++=
∑
∑∑
∑∑∑
≤≤
≤≤?≤≤
≤≤+?≤≤≤≤
1
11
1111
(5.5)
至此,我们找到一个新的解向量z。以下证明它的总价值不小于 y 的
总价值:
∑
∑∑
∑∑∑
≤≤
≤≤+≤≤
≤<≤≤≤≤
=
?
?
?
?
?
?
???+≥
???+=
ni
ii
kk
nik
iiikkk
ni
ii
ii
nik
iiikkkkk
ni
ii
ni
ii
yp
wpwzywyzyp
wpwzywpwyzypzp
1
11
11
/)()(
/)(/)(
(5.6)
中间的不等式是由于当I>k 时有p
k
/w
k
≥p
i
/w
i
而得。 但与 x的不同分量
的个数比 y与x的不同分量的个数至少减少一个。 以z 代替y 进行上
面的讨论,我们又可以找到新的解向量 'z ,如此等等,由于分量的个
数n有限,必到某一步停止,我们能找到解向量y*,它和x有相同的
分量,又与y又有相同的总价值(大于x的总价值) ,矛盾。 这个矛
盾源于x 不是最优解的假设。故,x 是最优解。
贪心算法主要用于处理优化问题。 每个优化问题都是由目标函数
和约束条件组成。满足约束条件的解称为可行解,而那些使得目标函
数取极值的可行解称为最优解。如 0/1 背包问题是一个优化问题,式
(5.3)中的函数是目标函数,而 (5.4)式描述的要求是约束条件,这里优
化是使目标函数取最大值。 贪心算法在每一步的决策中虽然没有完全
顾忌到问题整体优化,但在局部择优中是朝着 整体优化的方向发展
的。为此,贪心算法首先要确定一个度量准则,每一步都是按这个准
则选取优化方案。如 0/1 背包问题的贪心准则是 选取单位价值 p/w 最
大物品 ,而装载问题的贪心算法选取的准则是 选取最轻的货箱 ,找零
钱问题所用的贪心准则是 选取面值最大的硬币。 对于一个给定的问
题,初看起来,往往有若干中贪心准则可选,但在实际上,其中的多
数都不能使贪心算法达到问题的最优解。 如 0/1 背包问题下面的实例:
n=3, M=20, p=(25, 24, 15), w=(18,15,10)
如果以价值最大为贪心准则,则贪心算法的执行过程是:首先考虑将
物品 1 装包,此时获得效益值 25,包的剩余容量是 2。然后考虑将物
品 2 装包,但物品 2 的重量 15 超出包的剩余容量,只能装入该种物
品的 2/15,此时获得的总效益值为 25+24× 2/15=28.2。这样得到的可
行解( 1, 2/15, 0)并不是最优解。事实上,如果以单位价值最大为
贪心准则,则贪心算法的执行过程是:先计算出各个物品的单位价值
(25/18, 24/15, 15/10)=(1.389, 1.6, 1.5)。首先考虑单位价值大的物品装
包,即将物品 2 装包,此时获得效益值 24,背包的剩余容量是 5。然
后考虑装物品 3,由于物品 3 的重量超出背包的剩余容量,只能装入
该物品 5/15=1/2, 至此背包已经装满,所得的总的效益值为
24+15/2=31.5。比前面的装法的效益值大。实践证明,选择能产生最
优解的贪心准则是设计贪心算法的核心问题。 以下给出贪心算法流程
的伪代码。
贪心算法抽象化控制流程
Greedy(A, n) // A[1:n]代表那个输入
solution={}; //解向量初始化为空集
for i from 1 to n do
x=Select(A);
if Feasible(solution, x) then
solution=Union(solution, x);
endif
endfor
return(solution);
end Greedy
这里 Select(A)是按照谈心准则选取 A 种的输入项; Feasible(solution,
x)是判断已知的解的部分 solution与新选取的 x的结合 Union(solution,
x)是否是可行解。过程 Greedy 描述了用贪心策略设计算法的主要工
作和基本控制流程。 一旦给出一个特定的问题, 就可将 Select, Feasible
和 Union 具体化并付诸实现。
§2 作业排序问题
null 活动安排问题
我们首先从活动安排这一简单课题入手。 该问题要求高效地安排
一系列争用某一公共资源的活动。贪心算法提供了一个简单、漂亮的
方法使得尽可能多的活动能够兼容地使用公共资源。
问题: 已知 n 个活动 E={1, 2, … ,n }要求使用同一资源,第 k
个活动要求的开始和结束时间为 s
k
, f
k
, 其中 s
k
< f
k
, k=1, 2, … , n .
活动k 与活动j 称为相容的如果 s
k
> f
j
或者 s
j
> f
k
。活动安排问题就是
要在所给的活动集合中选出最大的相容活动子集 。
解决这个问题的基本思路是在安排时应该将结束时间早的活动
尽量往前安排,好给后面的活动安排留出更多的空间,从而达到安排
最多活动的目标。据此,贪心准则应当是:在未安排的活动中挑选结
束时间最早的活动安排。在贪心算法中,将各项活动的开始时间和结
束时间分别用两个数组 s 和 f 存储,并使得数组中元素的顺序按结束
时间非减排列: f
1
≤ f
2
≤… ≤ f
n
。
活动安排贪心算法伪代码
GreedyAction(s, f, n) // s[1:n]、 f[1:n]分别代表 n 项活动的起始
//时间和结束时间 , 并且满足 f[1]≤ f[2]≤… ≤ f[n]
j=1; solution={1}; //解向量初始化为空集
for i from 2 to n do
if s
i
≥f
j
then
solution=solution ∪ {j}; // 将 j 加入解中
j=i;
endif
endfor
return(solution);
end Greedy
例子 设待安排的 11 个活动的开始时间和结束时间按结束时间的
非减次序排列如下:
k 1 2 3 4 5 6 7 8 9 10 11
s[k] 1√ 3 0 5√ 3 5 6 8√ 8 2 12√
f[k] 4 5 6 7 8 9 10 11 12 13 14
解集合为 {1,4,8,11}.容易证明算法 GreedyAction 所得到的解是最
优解。
null 带限期作业安排问题
将活动排序问题加上效益条件,即 是下面的单机作业排序问题。
为使问题简化,我们假定完成每项作业的时间都是都是一样的,比如
都是 1。
问题: 已知 n 项作业 E={1, 2, … ,n }要求使用同台机器完成,
而且每项作业需要的时间都是 1。 第 k 项作业要求在时间 f
k
之前完成 ,
而且完成这项作业将获得效益 p
k
, k=1, 2, … , n 。 E 的子集称为相
容的,如果它们可以被安排由一台机器完成(该台机器在同一时刻至
多完成一个作业) 。带限期作业排序问题就是要在所给的作业集合中
选出总效益值最大的相容子集 。
这个问题可以看作是活动安排问题的推广, 作业 i 的起始时间
为 0≤s
i
≤f
i
-1. 而活动安排问题中的效益值可以认为全是 1。因此,很
容易想到贪心准则应该是尽量选取效益值最大的作业安排。 但由于起
始时间是一个区间, 可以将后面考虑的作业插到前面安排时剩余的空
闲时间片里,这是不同的地方。
带限期作业安排的贪心算法伪代码
GreedyJob(f, p, n) //f[1:n]和 p[1:n]分别代表各项作业的限期和效益
//值,而且 n 项作业的排序满足: p
1
≥ p
2
≥ … ≥ p
n
local J;
J={1};
for i from 2 to n do
if J ∪ {i} 中的作业是相容的 then
J= J ∪ {i}; // 将 i 加入解中
endif
endfor
end GreedyJob
定理 2 算法 GreedyJob 对于作业排序问题总是得到最优解。
证明:假设贪心算法所选择的作业的集合 J 不是最优解,则一定
有相容的作业子集 I,其产生更大的效益值。并假定 I 是具有最大效
益值的相容作业子集中使得 I ∩ J最大者。这两个作业集之间没有包
含关系。这是因为算法 GreedyJob 的特性和假定 I 产生的效益值比 J
的效益值更大。 假设 a 是 J\I 中具有最大效益的作业,于是, J 中比
a 具有更多效益的作业都应该在 I 中。对于任何元素 b∈I\J,我们有
p
a
≥p
b
。否则,由 p
a
<p
b
可推出 b∈J,因为按照算法 GreedyJob, b 将先
于 a 被考虑是否该加入 J 中。而 J 中那些效益比 a 效益大的作业也允
许将 b 加入 J,因为它们在 I 中是相容的。
如果用 S
I
和 S
J
分别记对应于 I 和 J 的调度表,并且 i∈I ∩ J, i 项
作业在 S
J
和 S
I
中分别属于时间段 [t, t+1]和 [t’, t’+1]。若 t<t’,则在 S
J
中将 i 项作业与 S
J
在时间段 [t’, t’+1]内排序的作业(如果有的化)交
换,这样得到的调度表 S
J
’还是相容的。同样讨论 t>t’情形。由此,我
们可以假设 I ∩ J 中作业在 S
I
和 S
J
中有相同的排序时间表。现在假设
排序时间表 S
J
中,作业 a 被安排在时间片 [t
a
, t
a
+1]中。则排序时间表
S
I
一定有某个作业 b∈I\J 被安排在时间片 [t
a
, t
a
+1]中。不然,在排序表
S
I
中,时间片 [t
a
, t
a
+1]空闲,将作业 a 安排在这个时间片调度,将会
得到效益值更大的相容作业子集,这与 I 的假设矛盾。现在,将 S
I
中的时间片 [t
a
, t
a
+1]换成调度作业 a,则得到新的相容作业集合
I’=(I\{b})∪{a}。显然 I’的效益值不低于 I 的效益值,但 I’ ∩ J 比 I ∩ J
的元素至少少一个,与前面的假设矛盾。 证毕
在上述算法中,每次新的作业 i 填入之前,都要做一次判断,实
际上是检查在 [0,1],[1,2], ?????, [f
i
-1,f
i
]中是否还有空闲的时间片?若有,
则选择其中之一安排作业 i;否则,作业 i 将被搁置。实现的方法有
两种。
第一种方法是基于问题的这样一个特点, 即每项作业都是在单位
时间内完成的,因此,只要关注截止期限的不增序列是否可以插入其
它的作业期限即可。 假设已经安排的作业的期限已经按递增的顺序排
好: )()()(
21 k
jDjDjD ≤≤≤L,则下一个具有期限 )(iD 的作业可
以插入 )()(
1+
≤
qq
jDjD 如果下述两个条件满足:
a). )(}1),(max{ iDqjD
q
≤+ ;
b). klqforljDjDiD
lq
≤<><
+
)(),()(
1
。
按这个准则,得到算法 GreedyInsertJob:
带限期作业的贪心插入算法
GreedyInsertJob(D, J, n, k) // D(1), … , D(n)是期限值。作业已按效
//益值 p
i
的不增顺序排列。 J[i]是最优解中第 i 个作业,终止时
//被选中的作业按期限值的不减次序排列 D[J[i]]≤ D[J[i+1]],
//1≤ i<k.
integer D[0:n], J[0:n], i, k, n, r ;
D[0]=J[0]=0; //初始化
k=1; J[1]=1; //初始记入作业
for i from 2 to n do
r=k;
while D[J[r]]>D[i] && D[J[r]]≠r do
r=r-1;
endwhile
if D[J[r]]≤D[i] && D[i]>r then //把作业 i 插入 J
for i from k by –1 to r+1 do
J[i+1]=J[i];
endfor
J[r+1]=I; k=k+1;
endif
endfor
end GreedyInsertJob
不难算出该算法的时间复杂度为 O(n
2
). 值得主要的是,这里大
部分的时间都浪费在插入作业时移动已经被选作业的位置上。 为了回
避这个问题,我们可以在逃选作业时就做“适当的安排” :使被考虑
的作业尽量地向后安排,这引导出第二种处理方法。
第二种方法:如果 J 是作业的可行子集(已经被选定的作业) ,
那么可以使用下述规则来确定这些作业中的每一个作业的处理时间:
若还没有给作业 i 分配处理时间,则分配给它时间片 ],1[ αα ? ,其中
α应尽量取大且使得时间片 ],1[ αα ? 是空的。为实现这种算法,我们
引入集合的不相交并运算 Union(i,j)和寻找元素 i 所属集合的程序
Find(i). 这里采用集合的树表示,根 k 代表它所在的集合。 Union(i,j)
是将以 i 为根的树所代表的集合与以 j 为根的树所代表的集合合并成
一个新的集合,新集合的树根是 i 或 j 依它们所在的集合的元素多少
而定(在此,我们取多者) 。为此,对每个集合都有一个统计该集合
元素个数的计数器, 一般是采取减的办法, 空集的初始计数器值为 0。
每添加一个元素该集合的计数器值减 1。
设有 n 个带期限的作业需要安排, ]}}[],...,1[max{,min{ nDDnb = ,
我们只需考虑时间片 [1,2], [2,3], … , [b-1, b]。为叙述方便,再加进时
间片 [0, 1]。考虑集合 U= {[0, 1] ,[1,2], [2,3], … , [b-1, b]}子集。为简
便,用 k 来代表时间片 [k, k+1]。初始被考虑的子集有 b+1 个,它们
分别是这 b+1 个元素构成的单元素子集 {k},代表它的树的根为
F(k)=k,元素个数计数器值为 P(k)=-1。
带期限作业的快速安排算法
GreedyTreeJob(D, n, b, J, k) //找最优解 J={J[1], J[2], … , J[k]},假
//定 p
1
≥ p
2
≥ ?????? ≥ p
n
, ]}}[],...,1[max{,min{ nDDnb =
integer b, D[n], J[n], F[0:b], P[0,b];
for i from 0 to n do
F[i]=I; P[i[=-1; // 将树初始化
endfor
k=0; // 初始化 J
for i=1 to n do //开始使用贪心准则
j=Find(min{n,D[i]});
if F[j]≠0 then
k=k+1; J[k]=I; //选择作业 i
s= Find(F[j]-1);
F[j]=F[s];
t=Union(s, j); P[t]=P[s]+P[j];
endif
endfor
end GreeduTreeJob
例子 设 n=7, (p
1
, p
2
,… , p
n
)=(35,30,25,20,15,10,5), (d
1
, d
2
,… ,
d
n
)=(4,2,4,3,4,8,3), 算法 GreedyTreeJob 的执行过程见文档“快速作业
调度” 。
null 多机调度问题
设有 n 项独立的作业 {1,2,…, n},由 m 台相同的机器加工处理。 作
业 i 所需要的处理时间为 t
i
。约定:任何一项作业可作任何一台机器
上处理, 但未完工前不准中断处理; 任何作业不能拆分更小的子作业。
多机调度问题要求给出一种调度方案, 使所给的 n 个作业在尽可
能短的时间内由 m 台机器处理完 。
这是一个 NP 完全问题,到目前为止还没有一个有效的解法。利
用贪心策略,有时可以设计出较好的 近似解。可以采用谈心准则: 最
长处理时间作业优先处理, 给出贪心算法(课后练习) 。
例子 设有 7 项独立的作业 {1,2,3,4,5,6,7},要由三台机器 M
1
, M
2
,
M
3
处理。各个作业所需要的处理时间分别为 {2,14,4,16,6,5,3}.将作业
按需时间的长短排列: [4, 2, 5, 6, 3, 7,1].首先将作业 4 安排给机器 M
1
处理,此时,机器 M
1
机器被占用的时间是 16,而机器 M
2
, M
3
被占
用的时间都是零,所以,应将作业 2 安排给机器 M
2
来处理,作业 5
应安排给机器 M
3
来处理,至此,作业 M
1
, M
2
, M
3
被占用的时间分
别是 16、 14、 6。接下去应安排作业 6,因为机器 M
3
能空闲下来的时
间最早,所以作业 6 应安排给机器 M
3
。此时,机器 M
1
, M
2
, M
3
能够
空闲下来的时刻分别是 16、 14、 11。所以下面将要安排的作业 3 应
安排给机器 M
3
。此时,机器 M
1
, M
2
, M
3
能够空闲下来的时刻分别是
16、 14、 15。 即将安排的作业 7 应安排给机器 M
2
, 此时, 机器 M
1
, M
2
,
M
3
能够空闲下来的时刻分别是 16、 17、 15。即将安排的作业 1 应安
排给机器 M
3
。如此全部作业均已安排完毕,所需要的时间是 17。这
里采用的调度方案是: 将需要时间最长的未被安排作业首先安排给能
够最早空闲下来的机器处理 。
M
1
16
M
2
14+3
M
3
6+5+4+2
§3 最小生成树
考虑无向图 G=(V, E),不妨假定该图代表城市间的交通情况,顶
点代表城市,边代表连接两个城市的可能的交通线路。现在将每条边
赋予一个权值,这些权可以代表建造该条线路的成本、交通线的长度
或其它信息。这样得到一个赋权图。在实际问题中,人们往往希望在
结构上选择一组交通线, 它们 连接所有的城市并且具有最小的建造总
成本 。这个问题相当于求取连通赋权图的具有最小权值的生成树。我
们称之为最优生成树。贪心方法可以很好地解决此类问题。在此介绍
两种算法: Prim 算法和 Kruskal 算法。
Prim 算法的基本思想是:
1). 选择图 G 的一条权值最小的边 e
1
;
作业 4
作业 2
作业 5 作业 6 3
7
1
2). 假设 G 的一棵子树 T 已经确定;
3). 选择 G 的不在 T 中的具有最小权值的边 e,使得 T∪{e}仍是
G 的一棵子树。
10 ( 1, 2)
1 Prim
30 45 50 ( 2, 6)
40 35 算
2 5 ( 6, 3)
25 法
20 4 55 3 ( 6, 4)
15
( 3, 5)
10 ( 1, 2)
1 Kruskal
30 45 50 ( 6, 3)
40 35 算
4 5 ( 6, 4)
25 法
20 3 55 2 ( 6, 2)
15
( 5, 3)
Kruskal 算法的基本思想是:
1). 选择图 G 的一条权值最小的边 e
1
;
2). 假设已经选好 G 的一组边 L={e
1
,e
2
,…,e
k
};
3). 选择 G 的不在 L 中的具有最小权值的边 e
k+1
,使得 T∪{e}诱
导出的 G 的子图不含 G 的圈。
Prim最小生成树算法伪代码
PrimTree(E,COST,a,T,mincost) //E是图G的边集,
1
2
4
5
6
3
4
5
6
3
1 2
//COST(n,n)是G 的带权邻接矩阵。计算一棵最小生成树T
//并把它作为一个集合存放到数组T[1:n-1,2]中,这棵树
//的权值赋给mincost
1 real COST[n,n],mincost;
2 integer NEAR[n],n,I,j,k,s,T[1:n-1,2];
3 (k,s)=具有最小权值的边;
4 mincost=COST(k,s);
5 for i from 1 to n do
6 if COST(I,s)<COST(I,k) then
7 NEAR(i)=s;
8 else NEAR(i)=k;
9 endif
10 endfor
11 NEAR(k)=NEAR(s)=0;
12 for i from 2 to n-1 do
13 设j是使得NEAR(j) ≠0且COST(j,NEAR(j))为最小的下标
14 (T[i,1],T[i,2])=(j,NEAR(j)); // 加入边 (j, NEAR(j))
15 mincost=mincost+COST(j,NEAR(j));
16 NEAR(j)=0;
17 for k from 1 to n do
18 if NEAR(k)≠0 && COST(k,NEAR(k))>COST(k,j) then
19 NEAR(k)=j;
20 endif
21 endfor
22 endfor
23 if mincost≥∞ then
24 print(‘no spanning tree’);
25 endif
26 end PrimTree
这里,树T的元素为(T[i,1],T[i,2]),其中1,2代表第i条边的两
个端点。NEAR(j)表示顶点j 的以最小权的边邻接的一个顶点。
PrimTree 算法的时间复杂度分析:语句 3 需要 O(|E|)的时间;
语句 17 至 21 的循环体需要时间 O(n),因而从语句 12 到 22 的循环
体共需要时间O(n
2
)。所以,PrimTree算法的时间复杂度为O(n
2
).
为叙述 Kruskal 算法,我们引进数据结构 min-堆:它是一个完
全二叉树,其中每个顶点都有一个权值,而且该二叉树满足:每个顶
点的权值都不大于其儿子的权值。min-堆的根具有最小的权值。
10
1
30 45 50 一个赋权图
40 35 和
4 5 它的边 min-堆
25 15
20 3 55 2 (1,2)
(6,4) (6,3)
(2,5) (3,5) (4,1) (6,2)
(2,3)(6,5)(5,1)
4
5
6
3
1 2
Kruskal最优生成树算法伪代码
KruskalTree(E,COST,n,T,mincost)//说明同算法PrimTree
1 real mincost, COST[1:n,1:n];
2 integer Parent[1:n],T[1:n-1],n;
3 以带权的边为元素构造一个min-堆;
4 Parent=-1;//每个顶点都在不同的集合中
5 i=mincost=0;
6 While i<n-1 && min-堆非空 do
7 从堆中删去最小权边(u,v)并重新构造min-堆;
8 j=Find(u); k=Find(v);
9 if j≠k then
10 i=i+1; T(i,1)=u; T(i,2)=v;
11 mincost=mincost+COST(u,v);
12 Union(j,k);
13 endif
14 endwhile
15 if i≠n-1 then
16 print(‘no spanning tree’)
17 endif
18 return
19 end KruskalTree
定理3 Kruskal 算法对于每一个无向连通图产生一棵最优树。
证明:设 T 是用Kruskal 算法产生的 G 的一棵生成树,而T’是
G的一棵最优生成树且使得|E(T’) ∩E(T)|最大。用E(T)表示树 T 的
边集,w(e)表示边 e 的权值,而边集 E 的权值之和用 w(E)表示。以
下证明w(E(T))=w(E(T’)).不妨设E(T) ≠E(T’),而e是E(T)\E(T’)
中权值最小的边。将 e 添加到 T’中即得到 T’的一个圈:e,
e
1
,e
2
,…,e
k
。因为T 是树,诸e
i
中至少有一个不属于 E(T)。不妨设
e
i
不属于E(T),则必然有w(e) ≤w(e
i
)。否则,w(e)>w(e
i
)和E(T)中比
e 权值小的边都在 T’中, e
i
同这些边一起不含有圈,因而,按
Kruskal 算法,e
i
将被选到 E(T)中。矛盾。在 T’中去掉边 e
i
,换上
边e,得到G的一棵新的生成树T”,这棵树有两个特点:a).T”权值
不大于 T’的权值,因而与 T’有相等的权值;b).|E(T”) ∩E(T)|>
|E(T’) ∩E(T)|.a)说明T”也是一棵最优树, 而b)说明与T’取法相
悖。因此w(E(T))=w(E(T’)),T 是最优生成树。证毕
§4 单点源最短路径问题
问题: 已知一个赋权有向图 G=(V,E,w),求由G中某个指定的顶
点v
0
其它各个顶点的最短的路径 。
45
50 10 路径 长度
(1) v0v2 10
10 35 (2) v0v2v3 25
20 15 20 30 (3) v0v2v3v1 45
15 3 (4) v0v4 45
一个加权有向图 从v
0
到其它各个顶点的最短路径
V
0
V
2
V
1
V
4
V
5
V
3
对于一般的单点源最短路径问题, 我们采用逐条构造最短路径的
办法, 用迄今已生成的所有路径长度之和为最小作为贪心准则, 为此,
每一条单独的路径都必须具有最小长度。 假定已经构造了k条最短路
径,则下面要构造的路径应该是下一条最短的最小长度路径。现在这
这k条最短路径的终点之集记为S,为陈述方便,也将 v
0
放于S 中。
如果V\S不是空集, 则从v
0
到V\S中顶点的最短路径中应该有一条最
短的,比如是v
0
到v
k+1
的最短路径P:
P=v
0
u
1
…u
s-1
u
s
v
k+1
(5.7)
显然,P
1
=v
0
u
1
…u
s-1
u
s
应是 v
0
到u
s
最短路径,因而由 S 的定义和我们
贪心准则,u
s
应属于S,同理,路径 P上其它顶点u
i
也都在S 中。所
以,新的最短路径一定是某个已有的最短路径向前延伸一步。如果用
Dist(v
i
)记从v
0
到 S 中顶点 v
i
的最短路径的长度,而图 G中的顶点w
依其属于 S与否分别记之为 S(w)=1或S(w)=0,则从v
0
出发,新的最
短路径的长度应该是
D(S)= )},()({min
0)(,1)(
wuCOSTuDist
wSuS
+
==
(5.8)
满足(5.8)式的顶点w被选择加入S, 新的最短路径就是从v
0
出发到 w
的最短路径,而此时的Dist(w)=D(S), S被被更新为S’=S ∪{w},后者
可以由更新w的集合特征值来实现:S(w)=1(原来S(w)=0).上述算法
思想是Dijkstra提出的。
Dijkstra最短路径算法伪代码
DijkstraPaths(v,COST,Dist,n)//G是具有n个顶点{1,2,…,n}
//的有向图, v 是G 中取定的顶点,COST 是G 的邻接矩阵
boolean S[1:n]; real COST[1:n,1:n],Dist[1:n];
integer u,v,n,num,i,w;
1 for i from 1 to n do
2 S(i)=0; Dist(i)=COST(v,i);
3 endfor
4 S(v)=1; Dist(v)=0;
5 for num from 2 to n do
6 选取顶点w使得 )}({min)(
0)(
uDistwDist
uS =
=
7 S(w)=1;
8 while S(u)=0 do
9 Dist(u)=min{Dist(u),Dist(w)+COST(w,u)};
10 endwhile
11 endfor
12 end DijkstraPath
这里,我们需要注意语句 6 和 9。根据(5.8)式,被选择的新的最短
路径的长度应该等于语句 6 中的 )}({min
0)(
uDist
uS =
,这可由语句 9 中的
Dist值更新语句看出。因为当S变成 S’=S ∪{w}后,Dist(u)(u不属
于S’)值如果改变, 则必然是由于从v到u的最短路径上含有顶点w,
又由于从 v 到 S’中其它顶点的最短路径不含有顶点 w,所以 w 是从
v 到 u 的最短路径上最后一个内部顶点,故
Dist(u)=min{Dist(u),Dist(w)+COST(w,u)}。 算法DijstraPath的正
确性由此可以保证。