首页 > 代码库 > 算法导论——lec 13 贪心算法与图上算法

算法导论——lec 13 贪心算法与图上算法

          之前我们介绍了用动态规划的方法来解决一些最优化的问题。但对于有些最优化问题来说,用动态规划就是“高射炮打蚊子”,采用一些更加简单有效的方法就可以解决。贪心算法就是其中之一。贪心算法是使所做的选择看起来是当前最佳的,期望通过所做的局部最优选择来产生一个全局最优解。

一、 活动选择问题

【问题】对几个互相竞争的活动进行调度:活动集合S = {a1, a2, ..., an},它们都要求以独占的方式使用某一公共资源(如教室),每个活动ai有一个开始时间si和结束时间fi ,且0 ≤ si < fi < ∞,一旦被选择,活动ai就占据半开时间区间[si, fi),如果区间[si, fi)和[sj fj)互不重叠(si ≥ fj 或sj ≥ fi ),称活动ai和aj是兼容的

【目标】找出一个最大的(活动数目最多的)相互兼容活动集合。

1、 动态规划的解法:

a、 最优子结构: 


Sij 是S中活动的子集,其中每个活动都在活动ai结束之后开始,且在活动aj开始之前结束。它们与ai和aj兼容,也与任何不迟于ai结束和不早于aj开始的活动兼容。

不妨假设活动已按照结束时间的单调递增顺序排序:f0≤ f1≤ f2≤ …≤ fn< fn+1,其中f0 = 0 and sn+1 = ∞.

显然,当i ≥ j时Sij = ?

子问题空间就是从Sij中选择最大兼容活动子集,其中0 ≤ i < j ≤ n + 1

b、 递归定义最优解

考虑某个非空子问题Sij,并假设它的解包含某活动ak,其中fi ≤ sk < fk ≤ sj,于是可以用活动ak生成两个子问题:Sik, Skj。

假设Sij的最优解Aij包含活动ak,则包含在Sij最优解中的针对Sik的解Aik和针对Skj的解Akj必定也是最优的。

于是:Aij =Aik∪{ak}∪Akj

设c[i, j]为Sij中最大兼容子集中的活动数,当Sij = ?,有c[i, j] = 0;当i ≥ j. 有c[i, j] = 0;如果ak在Sij的最大兼容子集中被使用,则子问题Sik和Skj的最大兼容子集也被使用,于是有递归式:c[i, j ] = c[i, k] + c[k, j ] + 1。

2、 贪心算法:将动态规划解转化为贪心解

【定理】对于任意非空子问题Sij,设am是Sij中具有最早结束时间的活动:fm = min {fk : ak ∈ Sij}.那么

1) 活动am在Sij的某最大兼容活动子集中被使用;

2) 子问题Sim为空,所以选择am将使子问题Smj成为唯一可能非空的子问题。

由上面的定理可知:只需要考虑一个子问题(另一个子问题一定为空),在解决子问题Sij时,我们只需考虑一种选择:在Sij中有着最早结束时间的那个活动,可以自顶向下解决每一个子问题:为解决子问题Sij,选择Sij中具有最早结束时间的活动am,并将子问题Smj的最优解中的活动集合加入到Sij的解中。

a、 递归贪心算法:

Recursive-Activity-Selector(s, f, i, j)
1 m<--i+1
2 while m < j and sm < fi
3 	do m<--m+1
4 if m < j
5 	then return {am}∪Recursive-Activity-Selector(s, f, m, j)
6 else return nil
b、 迭代贪心算法:容易发现上述递归算法是一个尾递归,很容易转化为迭代算法。

Greedy-Activity-Selector(s, f)
1 n<--length[s]
2 A<--{a1}
3 i<--1
4 for m<-- 2 to n
5 	do if sm >= fi //活动与选择的上一个活动兼容
6 		then A<--A∪{am}
7 		i<--m
8 return A

二、 贪心策略的基本内容

原理: 贪心算法是通过做一系列的选择来给出某一问题的最优解。对算法中的每一个决策点,做一个当时最佳的选择。

1、 上一节,我们开发一个贪心算法经历了如下的步骤:

a)、 决定问题的最优子结构;

b)、 设计出一个递归解;

c)、 证明在递归的任一阶段,最优选择之一总是贪心选择。那么,做贪心选择总是安全的;

d)、 证明通过做贪心选择,只有一个子问题非空;

e)、 设计出一个实现贪心策略的递归算法;

f)、 将递归算法转换成迭代算法

2、 上面的步骤过于繁琐,可以简化为:

a)、将优化问题转化成这样的一个问题,即先做出选择,再解决剩下的一个子问题;

b)、 证明原问题总是有一个最优解是做贪心选择得到的,从而说明贪心选择是安全的;

c)、 说明贪心选择后满足最优子结构,所以将子问题的最优解和我们所做的贪心选择联合起来,可以得出原问题的一个最优解。

3、 贪心算法的使用条件:贪心选择性质和最优子结构是两个关键的特点。如果我们能够证明问题具有这些性质,那么就可以设计出它的一个贪心算法。

a、 贪心选择性质: 一个全局最优解可以通过局部最优(贪心)选择来达到。

这一点是贪心算法不同于动态规划之处。动态规划中,每一步做出选择,这些选择依赖于子问题的解,先解子问题再做选择!我们必须证明在每一步所做的贪心选择最终能产生一个全局最优解。在证明中先考察一个全局最优解,然后证明可对该解加以修改,使其采用贪心选择,仍然是最优解。

贪心选择往往由某个值进行选择:固定的优先级;变化的优先级。

b、 最优子结构:对一个问题来说,如果它的一个最优解包含了其子问题的最优解,则称该问题具有最优子结构。

对比:



三、 Bellman-Ford算法

【问题】给出一个带权有向图G = (V, E),加权函数w : E → R为从边到实数权值的映射,求从给定点s到所有定点的单源最短路径(权值之和)。


1、 最短路径的最优子结构: 最短路径的子路径是最短路径。

2、 负边权值:如果图G=(V,E)不包含从源s可达的负权回路,则对所有v ∈V,最短路径的权的定义δ(s,v)依然正确。后面可以看到Dijkstra算法假设所有边权值非负Bellman-Ford算法允许输入图中存在负权边。

3、 关于回路:最短路径不能包含正权、负权回路,可以包含0权回路,但是没有必要。

4、 最短路径的表示: 前驱子图。算法结束时,Gπ就是最短路径树。最短路径并不唯一,因此最短路径树也是一样

5、 松弛技术: 对每个顶点v ∈ V,都设置一个属性d[v],用来描述从源点s到v的最短路径上权值的上界,称为最短路径估计

初始化最短路径估计:

Initialize-Single-Source(G, s)
1 for each vertex v in V[G]
2 	do d[v]<--INF
3 		Pi[v]<--nil
4 d[s]<--0

  松弛一条边(u,v)的过程中,测试是否可以通过u,对迄今找到的到v的最短路径进行改进,如果可以,则更新d[v]和π[v]。

Relax(u, v, w)
1 if d[v] > d[u] + w(u, v)
2 	then d[v]<--d[u] + w(u, v)
3 <span style="white-space:pre">	</span>Pi[v]<--u
松弛是改变最短路径和前驱的唯一方式,本章的算法的区别在于对每条边进行松弛操作的次数,以及对边执行松弛操作的次序有所不同。在Dijkstra算法以及关于有向无回路图的最短路径算法中,对每条边执行一次松弛操作。在Bellman-Ford算法中,对每条边要执行多次松弛操作。
最短路径及松弛的性质:

a、 三角不等式:对任意边(u,v) ∈E,有δ(s, v) <= δ(s, u)+w(u.v);

b、 上界性质: 对任意顶点v ∈V,有d[v] >= δ(s, v),而且一旦d[v]达到δ(s, v)值就不再改变。

c、 无路径性质: 如果从s到v不存在路径,则总有d[v]= δ(s, v)=INF

d、 收敛性质: 如果s-->u-->v是图G某u,v ∈V的最短路径,而且松弛边(u,v)之前的任何时间d[u]= δ(s, u),则操作过后总有d[v]= δ(s, v)。

e、 路径松弛性质: 如果p=<v0,v1,…,vk>是从s=v0到vk的最短路径,而且p的边按照(v0,v1),(v1,v2),…,(vk-1,vk)的顺序进行松弛,那么d[vk]=δ(s, vk)。这个性质的保持不受其他松弛操作的影响,即使它们与边p的边上的松弛操作混合在一起也是一样的。

6、 Bellman-Ford算法: 能在一般情况下(存在负边权的情况),解决单源最短路径问题。

Bellman-Ford(G, w, s)
1 Initialize-Single-Source(G, s)
2 for i <--1 to |V[G]|-1
3 	do for each edge (u, v) in E[G]
4 		do Relax(u, v, w)
5 for each edge (u, v) in E[G]
6 	do if d[v] > d[u] + w(u, v)
7 		then return False
8 return True
总的运行时间为 O(VE)。


四、 Dijkstra算法

1、 Dijkstra算法解决了有向图G=(V,E)上带权的单源最短路径问题,但要求所有边的权值非负

a、 假定对每条边(u,v)∈E,有w(u, v) ≥ 0;

b、 顶点集合S,从源点s到集合中的顶点的最短路径的权值均已确定。

c、 算法反复选择具有最短路径估计的顶点u∈ V-S,并将u加入到S中,对u的所有边进行松弛。

d、 Q是顶点的最小优先队列,排序关键字为顶点的d值

Dijkstra(G, w, s)
1 Initialize-Single-Source(G, s)
2 S<--Nil
3 Q<--V[G]
4 while Q != nil
5 	do u<--Extract-Min(Q)
6 	S<--S ∪ {u}
7 	for each vertex v ∈ adj[u]
8 		do Relax(u, v, w)
因为Dijkstra算法总在V-S中选择“最轻”或“最近”的顶点插入到集合S中,所以我们说它使用了贪心策略
每当一个顶点u被插入集合S中时,有d[u] =δ(s,u)。

2、 定理:已知一带权有向图G=(V,E),其加权函数w的值为非负,源点为s。对该图运行Dijkstra算法,则在算法终止时,对所有u ∈ V有d[u]=δ(s,u)。

3、 Dijkstra算法运行时间分析:

a、 其中调用了三种优先级队列操作: INSERT(第3行)|V|次, EXTRACT-MIN(第5行) |V|次, DECREASE-KEY(第8行调用的RELAX中)所有的for循环中有共计|E|次迭代。

b、 算法运行时间依赖于最小优先级队列的具体实现: 简单的将d[v]存入一个数组的第v项,INSERT和DECREASE-KEY操作都是O(1)时间,EXTRACT-MIN操作是O(V)时间。总的时间为O(V^2+E+V) = O(V^2).

选用不同的优先级队列操作:


Time = n X ( T(insert )+ T(extract-min)) + m x T(decrease-key),其中(|v| = n,  |E| = m)


五、 贪心算法的理论基础: 拟阵

贪心方法可解的问题当中有一部分是可以通用“拟阵”来解的, 虽然拟阵没有覆盖贪心方法所适用的所有情况(例如活动选择问题和赫夫曼编码问题),但是它覆盖了许多具有实际意义的情况, 并且发展很快,并正在覆盖更多的应用。

1、 拟阵: 

满足下列条件的一个序对M = (S,?):

1) S是有限非空集合

2) ?是S的一个非空子集族,称为S的独立子集,使得如果B ∈?且A ? B,那么A ∈?。我们说?是遗传的,如果它满足这个性质。(空集?必为?的一个成员)

3) 如果A ∈?, B ∈?,且|A| < |B|,则有某个元素x ∈ B - A使得A∪{x} ∈?。我们称M满足交换性质

2、 加权拟阵的贪心算法:

Greedy(M, w)
1 A<--nil
2 sort S[M] into monotonically decreasing order by weight w
3 for each x in S[M], taken in monotonically decreasing order by weight w
4 	do  if A∪{x}∈l[M]
5 		then A<--A∪{x}
6 return A
GREEDY的运行时间:n = |S|

1) GREEDY的排序阶段的时间为O(n lgn);

2) 第4行中对S中的每个元素执行一次,共n次;

3) 在第4行的每次执行中要检查A∪{x}是否独立;

4) 如果每次检查的时间为O(f(n)),则整个算法的运行时间为O(n lg n + nf (n))。

适宜用贪心方法来获得最优解的许多问题,都可以归结为在加权拟阵M = (S,?)中,找出一个具有最大权值w(A)的独立子集A∈?的问题。