算法导论——lec 11 动态规划及应用

和分治法一样,动态规划也是通过组合子问题的解而解决整个问题的。分治法是指将问题划分为一个一个独立的子问题,递归地求解各个子问题然后合并子问题的解而得到原问题的解。与此不同,动态规划适用于子问题不是相互独立的情况,即各个子问题包含公共的子子问题。在这种情况下,如果用分治法会多做许多不必要的工作,重复求解相同的子子问题。而动态规划将每个子问题的解求解的结果放在一张表中,避免了重复求解。

一、 动态规划介绍

1、 动态规划方法介绍: 动态规划主要应用于最优化问题, 而这些问题通常有很多可行解,而我们希望从中找出具有最优值的解,即最优解。

2、 设计一个动态规划程序算法的过程:

a、 刻画最优解的结构;

b、 递归定义最优解的值;

c、 按自底向上的方式计算最优解的值;

d、 由计算结果构造一个最优解。

二、 装配线调度问题

问题: 已知两条装配线如上图所示,每个装配站所用时间为aij,要求输出装配一辆汽车所需的最短时间。

分析: 给定一个解,我们可以在Θ(n)时间内计算出所需要的时间,但是这样的解总共有2^n个,穷举所有的解来找出最优解是不明智的。我们用动态规划来解决问题。

步骤一:描述最优解的结构的特征

假设通过装配站S1j的最快路线经过了S1(j-1),那么这条路线必定利用了从开始点到S1(j-1)的最快路线。否则如果有更快的路线到达S1(j-1),那么我们就能找到到达S1j的更快的路线。同理,如果通过装配站S1j的最快路线经过了S2(j-1),那么这条路线也必定是利用了从开始点到S2(j-1)的最快路线。

更一般地:如果一个问题的最优解包含了子问题的一个最优解,那我们称这个性质为最优子结构。

有了最优子结构的性质,我们就可以利用自问题的最优解构造原问题的最优解。因为通过装配站S1j的最快路线,必定要经过第1或第2条路线的第j-1个站点,因此,通过装配站S1j的最快路线必定利用了从开始点到S1(j-1)然后直接到S1j,或者从开始点到S2(j-1)然后从装配线2到装配线1而到达S1j。同理,到达S2j的最快路线也是类似的过程。所以对于装配线问题,通过建立子问题的最优解,就可以建立原问题某一个实例的最优解。

步骤二:利用子问题的最优解递归定义一个最优解的值

我们选择在两条装配线上,通过装配站 j 的最快路线问题做为子问题,j = 1,2,...,n。令f i[j] 表示一个底盘从起点到装配站Sij的最快可能时间。我们最终的目标是确定底盘通过工厂的所有路线的最快时间 f* 。底盘必须一路经由装配线1或2通过装配站n,然后到达工厂出口。由于这些路线的最快者就是通过整个工厂的最快路线,有 f* = min(f1[n] + x1, f2[n] + x2)。而f11 = e1 + a11, f21
= e2 + a21,

f1[j] = min { f1[j-1] + a1j, f2[j-1] + t2(j-1) + a1j }

f2[j] = min { f1[j-1] + t1(j-1) + a2j, f2[j-1] + a2j }

合并上面的公式,得到:

步骤三: 按照自底向上的方式计算最优解的值

根据上面的公式,我们逐步求得子问题的最优解如下:

步骤四: 由计算结果构造一个最优解

由上面求得的fi[j] 和 li[j],我们可以构造出一个最优解。

Fastest-Way(a, t, e, x, n)
1 f1[1] <-- e1 + a11
2 f2[1] <-- e2 + a21
3 for j <-- 2 to n
4 	do if f1[j-1] + a1j <= f2[j-1] + t2(j-1) + a1j
5 		then f1[j] <-- f1[j-1] + a1j
6 		I1[j] <-- 1
7 	else f1[j] <-- f2[j-1] + t2(j-1) + a1j
8 		I1[j] <-- 2
9 	if f2[j-1] + a2j <= f1[j-1] + t1(j-1) + a2j
10		then f2[j] <-- f2[j-1] + a2j
11		I2[j] <-- 2
12	else f2[j] <-- f1[j-1] + t1(j-1) + a2j
13		I2[j] <-- 1
14if f1[n] + x1 <= f2[n] + x2
15	then f* <-- f1[n] + x1
16	<span style="white-space:pre">	</span>I* <-- 1
17	else f* <-- f2[n] + x2
18		I* <-- 2

计算出f和I以后,需要构造在通过工厂的最快的路线中使用的装配站的序列,下面过程以序号递减的顺序输出各个装配站:

Print-Station(I, n)
1 i <-- I*
2 print "line " i ", station " n
3 for j <-- n downto 2
4 	do i <-- Ii[j]
5 		print "line " i ", station " j-1

顺着计算倒着找。

三、 矩阵链乘法

矩阵连乘积的最优计算次序问题:对于给定的n个矩阵{A1 ,A2 ,…,An }(其中Ai的维数为pi-1×pi ,i=1,2,…,n),如何确定计算矩阵连乘积A1 A2 …An的一个计算次序,使得依此次序计算矩阵连乘积需要的数乘次数最少。

解决这个问题最容易想到的是穷举搜索,列出所有可能的次序计算乘积次数,选出次数最小的方案。事实上,对于n个矩阵的连乘积,设有P(n)个不同的计算次序,那么P(n)满足如下的递推关系式:

P(n)实际上是Catalan数, 即P(n)=C(n-1) :(C(n)定义如下)

可以证明,P(n)的下界为2^n。很明显,穷举的方法并不可取。

下面我们用动态规划的方法来解这个问题:

步骤一: 寻找最优的子结构

问题的一个关键特征是:计算A1...n的一个最优加括号把乘积在Ak和Ak+1之间断开,则前缀子链A1...k的加括号也是最优的,同样后缀子链Ak+1...n的加括号也是最优的。

步骤二: 利用子问题的最优解递归定义一个解的值

设Ai…j ,1≤i≤j≤n,所需的最少标量乘法次数为m[i,j],原问题的最优值为m[1,n]。m[i,j]可以递归地定义为:

步骤三: 按照自底向上的方式计算最优解的值

根据m[i,j]的递归定义,容易写一个递归程序来计算m[1,n]。简单地递归计算将耗费指数计算时间。然而, 我们注意到,在递归计算过程中,不同的子问 题个数只有θ(n^2)个。由此可见,在递归计算时,许多子问题被重复计算多次。这也是该问题可用动态规划算 法求解的又一显著特征。

用动态规划算法解此问题,可以用自底向上的方式进行计算,在计算过程中,保存已解决的 子问题答案,每个子问题只计算一次,而在后 面需要时只要简单查一下,从而避免大量的重 复计算,最终得到多项式时间的算法。

该算法填表m的方式对应于求解按长度递增的矩阵链上的全部括号问题。

步骤四: 由计算结果构造一个最优解

m[i,j]给出了最优值,即计算Ai…j所需的最少数乘次数。同时还确定了计算Ai…j的最优次序中的断开位置k,也就是说,对于这个k有m[i,j] = m[i,k] + m[k+1,j] + pi-1 pk pj 。若将对应于m[i,j]的断开位置k记录在s[i,j]中,则相应的最优解便可递归地构造出来。

Matrix-Chain-Order(p)
1 n<--length[p] - 1
2 for i <-- 1 to n
3 	do m[i, i]<--0
4 for l <-- 2 to n
5 	do for i <-- 1 to n
6 	<span style="white-space:pre">	</span>do j <-- i+l-1
7 		<span style="white-space:pre">	</span>m[i, j]<--INF
8 			for k<--i to j-1
9 				do q<--m[i,k] + m[k+1,j] + p[i-1]*p[k]*p[j]
10				if q < m[i, j]
11					then m[i, j]<--q
12					     s[i, j]<--k
13 return m and s

分析:MATRIX-CHAIN-ORDER具有嵌套循环结构, 运行时间为O(n3)。算法还需要Θ(n2) 空间存放m和s。

MATRIX_CHAIN_ORDER己记录了构造一个最优解所需要的全部信息。

Print-Optimal-Parents(s, i, j)
1 if i = j
2 	do print "Ai"
3 else print "("
4 	Print-Optimal-Parents(s, i, s[i,j])
5 	Print-Optimal-Parents(s, s[i, j]+1, j]
6 	Print ")"

四、 动态规划问题的关键特征

上面我们介绍了如何用动态规划的方法来解决一些问题,那么当我们遇到一个问题的时候,我们如何判断是否可以用动态规划的方法来求解呢?这里我们介绍使用动态规划程序设计方法的最优化问题的两个要素:最优子结构和重叠子问题。我们还要讨论备忘录方法来充分利用重叠子结构的性质。

1、 设计动态规划算法的第1步通常是要刻划最优解的结构。当问题的最优解包含了其子问题的最优解时,称该 问题具有最优子结构性质。问题的最优子结构性质提供了该问题可用动态规划算法求解的重要线索。

2、 子问题空间: 为了描述子问题空间,可以遵循这样一条有效的经验规则,就是尽量保持这个空间简单,然 后在需要时再扩充它。

3、 最优子结构在问题域中以两种方式变化:有多少个子问题被使用在原问题的一个最优解中; 在决定一个最优解使用哪些子问题时有多少种选择。

4、 一个动态规划算法的运行时间依赖于上面讨论的两个因素的乘积:子问题的总个数和每一个 子问题中有多少选择。

a、 在装配线调度中,共有Θ(n)个子问题,并且只有两个选择来检查每个子问题,所以执行时间为Θ(n)。

b、 对于矩阵乘法,共有Θ(n^2)个子问题,在每个子问题中又至多有n-1个选择,因此执行时间为O(n^3)

5、 动态规划以自底向上的方式来利用最优子结构:首先找到子问题最优解,然后找到问题的 一个最优解。问题解的代价往往是子问题的代 价加上选择本身带来的代价。

6、 要注意在不能使用最优子结构的时候,就一定不能假设它能够使用。考虑下面两个问题:已 知一个有向图G=(V,E)和结点u, v∈V

a、 无权最短路径:找出一条从u到v的包含最少边数的路径。这样的一条路径必须是简单路径,因为从路 径中去掉一个回路后,会产生边数更少的路径。--最优子结构

b、 无权最长简单路径:找出一条从u到v的包含最多边数的简单路径。我们需要加入简单性的要求。否 则,就可以随意地遍历一个回路任意多次,来得到 有任意多的边数的路径。--非最优子结构(事实上,这个问题不能用动态规划解,是NP完全的)

根本的区别在于:a的子问题之间是相互独立的,b的子问题是不独立的。

7、 子问题独立:子问题的解不会影响同一问题中另一个子问题的解。

8、可用动态规划算法求解的问题应具备的另一基本要素是子问题的重叠性质。也就是说,在用递归算法自顶向下解此问题时,每次产生的子问题并不总是新问题,有些 子问题被反复计算多次。

动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只解一次,而后将其解保存在一个表格中, 当再次需要解此子问题时,只是简单地用常数时间查看 一下结果。通常,不同的子问题的个数随输人问题的大 小呈多项式增长。因此,用动态规划算法通常只需要多 项式时间,从而获得较高的解题效率。

当解某一问题的直接递归算法所产生的递归树中,相同的子问题反复出现,并且不同子问题的 个数又相对较少时,用动态规划算法是有效的

9、 动态规划要求其子问题既要独立又要重叠: 如果同一问题的两个子问题不共享资源,则它们就是独立的; 对两个子问题来说,如果他们确实是相同的子问题,只是作为不同问题的子问题出现的 话,是重叠的,则它们是重叠的。

10、动态规划算法的一个变形是备忘录方法:

a、与动态规划算法一样,备忘录方法用一个表格来保存已解决的子问题的答案,在再碰到该子 问题时,只要简单地查看该子问题的解答,而 不必重新求解;

b、 不同的是,备忘录方法采用的是自顶向下的递归方式,而动态规划算法采用的是自底向上的
非递归方式。

c、 备忘录方法的控制结构与直接递 归方法的控制结构相同,区别仅在于备忘录方 法为每个解过的子问题建立了备忘录以备需要 时查看,避免了相同子问题的重复求解。

11、 备忘录方法:备忘录方法为每个子问题建立一个记录项,初始化时,该记录项存入一个特殊的值,表示该 子问题尚末求解。

a、 在求解过程中,对碰到的每个子问题,首先查看其相应的记录项。若记录项中存储的是初始化时存入 的特殊值,则表示该子问题是第一次遇到,此时需 要对该子问题进行求解,并把得到的解保存在其相 应的记录项中,以备以后查看。

b、 若记录项中存储的不是初始化时存入的特殊值,则表示该子问题己被求解过,只要从记录项中取出 读子问题的解答即可。

Memoized-Matrix-Chain(p)
1 n <-- length[p]-1
2 for i<--1 to n
3 	do for j<--i to n
4 		do m[i, j]<--INF
5 return Lookup-Chain(p, 1, n)

Lookup-Chain(p, i, j)
1 if m[i,j] < INF
2 	then return m[i,j]
3 if i = j
4 	then m[i,j]<--0
5 else for k<--i to j-1
6 	do q<--Lookup-Chain(p, i, k) + Lookup-Chain(p, k+1, j) + Pi-1*Pk*Pj
7 		if q < m[i,j]
8 			then m[i,j]<--q
9 return m[i, j]

与动态规划算法MATRIX_CHAIN_ORDER一样, 备忘录算法MEMOIZED_ MATRIX_CHAIN用数组m[1…n,1…n]的单元m[i,j]来记录解子问题Ai…j的最优计算量。MEMOIZED_MATRIX_CHAIN耗时O(n3)。

综上所述,矩阵连乘积的最优计算次序问题可用自顶向下的备忘录算法或自底向上的动态规划算法在O(n3)时间内求解。这两个算法都利用了子问题重叠性质。总共有θ(n2)个不同的子问题。对每个子问题,两种方法都只解一次,并 记录答案,再碰到该问题时,不重新求解而简单地取用已 得到的答案。因此,节省了计算量,提高了算法的效率。

12、 动态规划和备忘录方法:

a、 一般地讲,当一个问题的所有子问题都至少要解一次时, 用动态规划算法解比用备忘录方法好。此时,动态规划算 法没有任何多余的计算。

b、 当子问题空间中的部分子问题可不必求解时,用备忘录方法则较有利,因为从其控制结构可以看出,该方法只解那 些确实需要求解的子问题。

五、 最长公共子序列

问题: 最长公共子序列(LCS)问题:给定两个序列X=<x1 , x2 , …, xm >和Y=<y1 , y2 , … , yn >,要求找出X和Y的一个最长公共子序列。

1、 最长公共子序列的结构: LCS的最优子结构性质

设序列X=<x1 , x2 , …, xm >和Y=<y1 , y2 , …, yn >的一个最长公共子序列Z=<z1 , z2 , …, zk >

a、 
若xm = yn,则zk = xm = yn,且Zk-1是Xm-1和Yn-1的一个LCS;

b、 若xm <> yn,则zk <> xm蕴含Z是Xm-1和Y的一个LCS;

c、 若xm <> yn,则zk <> yn 蕴含Z是X和Yn-1的一个LCS。

2、 一个递归解:由最长公共子序列问题的最优子结构性质可知,要找出X=<x1 , x2 , …, xm >和Y=<y1 , y2 , …, yn >的最长公共子序列,可按以下方式递归地进行

a、 当xm =yn 时,找出Xm-1 和Yn-1 的最长公共子序列,然后在其尾部加上xm (=yn )即可得X和Y的一个最长公共子序列。

b、 当xm ≠yn 时,必须解两个子问题,即找出Xm-1 和Y的一个最长公共子序列及X和Yn-1 的一个最长公共子序列。这两个公共子序列中较长者即为X和Y的一个最长公共子序列。

由此递归结构容易看到最长公共子序列问题具有子问题重叠性质。例如,在计算X和Y的最长公共子序列时,可能要计算出X和Yn-1及Xm-1和Y的最长公共子序列。而这两个子问题都包含一个公共子问题,即计算Xm-1和Yn-1的最长公共子序列。

以c(i, j)表示Xi和Yj的最长公共子序列的长度。

3、 按照自底向上的方式计算最优解的值:

LCS_LENGTH(X,Y)以序列X=<x1 , x2 , …, xm >和Y=<y1 , y2 , …, yn >作为输入。输出两个数组c[0..m ,0..n]和b[1..m ,1..n]。其中c[i,j]存储Xi与Yj的最长公共子序列的长度,b[i,j]记录指示c[i,j]的值是由哪一个子问题的解达到的,这在构造最长公共子序列时要用到。最后,X和Y的最长公共子序列的长度记录于c[m,n]中。

由于c[i,j]需要用到c[i-1,j-1], c[i-1,j]和c[i, j-1]的值,所以我们可以一行一行地计算c中元素的值。

LCS-Length(X, Y)
1 m<--length[X]
2 n<--length[Y]
3 for i<--1 to m
4 	do c[i, 0]<--0
5 for j<--1 to n
6 	do c[0, j]<--0
7 for i<--1 to m
8 	do for j<--1 to n
9 		do if xi = yj
10			then c[i, j]<--c[i-1, j-1] + 1
11				b[i,j]<--"\"
12			else if c[i-1, j] > c[i, j-1]
13				c[i,j]<--c[i-1,j]
14				b[i,j]<--"|"
15			else c[i,j]<--c[i,j-1]
16				b[i,j]<--"——"
17return c and b

算法的复杂性为O(mn)

4、 由计算结果构造一个最优解

由算法LCS_LENGTH计算得到的数组b可用于快速构造序列X=<x1 , x2 , …, xm >和Y=<y1 , y2 , …, yn >的最长公共子序列。首先从b[m,n]开始,沿着其中的箭头所指的方向在数组b中搜索。当b[i,j]中遇到""时,表示Xi与Yj的最长公共子序列是由Xi-1与Yj-1的最长公共子序列在尾部加上xi 得到的子序列;当b[i,j]中遇到"↑"时,表示Xi与Yj的最长公共子序列和Xi-1与Yj的最长公共子序列相同;当b[i,j]中遇到"←"时,表示Xi与Yj的最长公共子序列和Xi与Yj-1的最长公共子序列相同。

Print-LCS(b, X, i, j)
1 if i = j
2 	do return
3 if b[i, j] = "\"
4 	do Print-LCS(b, X, i-1, j-1)
5 		print xi
6 	else if b[i, j] = "|"
7 		Print-LCS(b, X, i-1, j)
8 	else if b[i,j] = "——"
9 		Print-LCS(b, X, i, j-1)

在算法LCS中,每一次的递归调用使i或j减1,因此算法的计算时间为O(m+n) 。

5、 算法改进:

a、  在算法LCS_LENGTH和LCS中,可进一步将数组b省去。事实上,数组元素c[i,j]的值仅由c[i-1,j-1],c[i- 1,j]和c[i,j-1]三个值之一确定,而数组元素b[i,j]也只是用来指示c[i,j]究竟由哪个值确定。因此,在算法LCS中, 我们可以不借助于数组b而借助于数组c本身临时判断c[i,j]的值是由c[i-1,j-1],c[i-1,j]和c[i,j-1]中哪一个数值元素所确定,代价是Ο(1)时间。既然b对于算法LCS不是必要的,那么算法LCS_LENGTH便不必保存它。这一来,可节省θ(mn)的空间,而LCS_LENGTH和LCS所需要的时间分别仍然是Ο(mn)和Ο(m+n)。不过,由于数组c仍需要Ο(mn)的空间,因此这里所作的改进,只是在空间复杂性的常数因子上的改进。

b、 如果只需要计算最长公共子序列的长度,则算法的空间需求还可大大减少。事实上,在计算c[i,j]时,只用到数组c的第i行和第i-1行。因此,只要用2行的数组空间就可以计算出最长公共子序列的长度。更进一步的 分析还可将空间需求减至min(m, n)。

六、 最优二叉查找树

问题:给定一个由n个互异的关键字组成的序列K=<k1 , k2 , ..., kn >,且关键字有序,我们从这些关键字中构造出一颗二叉查找树。对每个关 键字ki,一次搜索为ki的概率是pi。某些搜索的值可能不在K内,因此还有n+1个“虚拟键”d0 , d1 ,
d2 , ..., dn代表不在K内的值。具体地,d0代表所有小于k1的值,dn 表示所有大于kn的值,而对于i = 1, 2, ..., n -1,虚拟键di 代表ki和ki+1之间的值。对每个虚拟键di , 一次搜索对应于di的概率是qi。

可以看到n个节点的二叉树共有:Ω(4^n/n^1.5)

1、 最优子结构:如果一棵最优二叉查找树T有一颗包含关键字ki , ..., kj的子树T‘,那么这棵子树T‘ 对于关键字ki , ..., kj 和虚拟键di-1 , ..., dj的问题也必定是最优的。

2、 递归解:

给定关键字ki , ..., kj,其中之一假设是kr (i≤r≤j),将是包含这些键的一棵最优子树的根。根kr 的左子树包含关键字ki , ..., kr-1 (和虚拟键di-1 ,...,dr-1 ),右子树包含关键字kr+1 , ..., kj (和虚拟键dr , ..., dj ). 只要我们检查所有的根kr,其中i ≤r ≤j,而且确定所有包含关键字ki, ..., kr-1和kr+1, ..., kj的最优二叉查找树,就可以保证找到一棵最优的二叉查找树。

选取子问题域为找一个包含关键字ki , ..., kj的最优二叉查找树,其中i ≥1, j ≤n, 且j ≥i –1, j=i-1意味着没有真实的关键字,只有虚拟键di-1。

定义e[i, j] 为搜索一棵包含关键字ki , ..., kj的最优二叉查找树的代价。最终,我们希望计算e[1,n]。

如果kr 是一颗包含关键字ki , ..., kj的最优子树的根,则有

将e[i,j]重写:

我们选择有最低期望搜索总代价的结点作为 根,从而得到最终的递归公式:

3、计算 最优解的值:

把e[i,j]保存在表e[1 ‥n + 1, 0 ‥n]中。第一维的下标需要达到n+1而不是n,原因是为了有一个只包含虚拟键dn, 需要计算和保存e[n + 1, n]。

使用j ≥i -1的表项e[i, j], 表root[i, j]来记录包含关键字ki , ..., kj的子树的根。这个表只使用1≤i≤j≤n的表项。

为了提高效率,还需要一个表格,不是每当计算e[i,j]时都从头计算w(i,j)(这将用Θ(j - i)步加法) ,而是把这些值保存在w[1‥n+1, 0‥n] 中。对基础情况,w[i, i-1]=qi-1,其中1≤i≤n。对j ≥i,计算

可以计算出Θ(n2)个w[i, j],每个需要Θ(1)。

Optimal-BST(p, q, n)
1 for i<--1 to n+1
2 	do e[i, i-1]<--qi-1
3 		w[i, i-1]<--qi-1
4 for l<--1 to n
5 	do for i<--1 to n-l+1
6 		do j<--i+l-1
7 			e[i,j]<--INF
8 			w[i,j]<--w[i,j-1]+pj+qj
9 			for r<--i to j
10				do t<--e[i, r-1] + e[r+1, j] + w[i,j]
11					if t < e[i, j]
12						then e[i, j]<--t
13						root[i,j]<--r
14retrn e and root

分析: 与MATRIX-CHAIN-ORDER一样,for有三层嵌套,每个循环下标至多n个值。所以有O(n^3);最优二叉树查找和矩阵链乘积中的循环下标并没有刚好相同的界,但是他们在所有方向上都 至多差1。所以如同矩阵链乘积问题,最优二叉查找树的的下界也为Ω(n^3);故OPTIMAL-BST 过程需要Θ(n^3)的运行时间。

4、 构造最优解

由上述Optimal-BST得到的e和root,我们可以知道最优解的值,由root可以知道如何构造一个最优解。

七、 每对顶点间的最短路径

问题: 讨论图上每对顶点间的最短路径问题

a、 可以通过单元最短路径算法运行|V|次来解决每对定点见的最短路径问题。

b、 本节要介绍一个动态规划算法,用来解决有向图G=(V,E)上每对顶点间的最短路径问题。动态规划的每一次主循环都将引发一个与矩阵乘法 运算十分相似的操作,因此算法看上去很像是 重复的矩阵乘法。算法的运行时间为 Θ(V3lgV)。另一种动态规划算法Floyd-Warshall算法,该算法的运行时间为Θ(V3)。

算法一:

1、 假设图以邻接矩阵W=(wij )来表示。考察从顶点i到顶点j的一条最短路径p,假设p至多包含m条边。假设途中不存在权值为负的回路,则m必是有限值。如果i=j,则路径p权值为0而且没有边。若顶点i和顶点j是不同顶点,则把路径p分解为 ,其中路径p‘至多包含m-1条边。由引理24.1,p‘是从i到k的一条最短路径,因而δ(i, j) = δ(i, k) + wkj。

2、 现在设是从顶点i到顶点j的至多包含m条边的任何路径的权值最小值。当m=0时,从i到j存在一条不包含边的最短路径当且仅当i=j。因此

3、 对m ≥1,先计算L(m-1, i, j)(从i到j的最短路径的权至多包含m-1条边),以及从i到j的最多包含m条边的路径的最小权值,后者是通过计算j的所有可能前趋k而得到的,然后取二者中的最小值作为L(m, i, j),因此我们递归定义

4、 把矩阵W= (wij )作为输入,来计算一组矩阵L(1), L(2),..., L(n-1),最后矩阵L(n-1) 包含实际的最短路径权值。注意对所有的顶点i, j∈V, L(1, i, j)= wij,因此L(1)=W。

5、 算法如下所示,给定矩阵L(m-1)和W,返回矩阵L(m) 。也就是它把已经计算出来的最短路径延长一条边。

Extend-Shortest-Path(L, W)
1 n-<rows[L]
2 Let L' be an nXn matrix
3 for i<--1 to n
4 	do for j<--1 to n
5 		do L'(i, j)<--INF
6 			for k<--1 to n
7 				do L'(i, j)<--min(L'(i, j), L(i,k)+wj)
8 return L'

算法运行时间为:Θ(n^3)。

6、 如果对过程EXTEND-SHORTEST- PATHS进行这样的变换,并用0(表示+)替换∞(表示min),得到一个直接的Θ(n3)的矩阵乘法过程:


设A·B 代表过程EXTEND-SHORTEST-PATHS(A, B)返回的矩阵乘积,我们计算n-1个矩阵的序列

与前面的论证一样,矩阵L(n-1) = Wn-1包含最短路径的权值。

7、 下面的过程在Θ(n^4)时间内计算该序列

Slow-All-Pairs-Shortest-Paths(W)
1 n<--rows[W]
2 L(1)<--W
3 for m<--2 to n-1
4 	do L(m)<--Extend-Shortest-Path(L(m-1), W)
5 return L(n-1)

算法改进:我们的目标并不是计算出全部的L(m)矩阵,我们仅仅需要矩阵L(n-1)。

通过计算下列矩阵序列,只 需计算?lg(n -1)?个矩阵乘积就能计算出L(n-1)

下面过程利用重复平方技术计算上述矩阵序列:

Fast-All-Pairs-Shortest-Paths(W)
1 n<--rows[W]
2 L(1)<--W
3 m<--1
4 while m < n-1
5 	do L(2m)<--Extend-Shortest-Path(L(m), L(m))
6 		m<--2*m
7 return L(m)

分析: 因为?lg(n -1)?个矩阵乘积中的每一个都需要Θ(n3) 时间,因此FASTER-ALL-PAIRS-SHORTEST- PATHS的运行时间为Θ(n^3lgn)。算法中不包含复杂数据结构,因此隐含于Θ中的常数很小。

算法二: Floyd-Warshall算法——算法允许存在负值的边,但我们假设没有权值为负的回路。

1、 设G的顶点为V={1, 2,..., n},对某个k考虑顶点的一个子集{1, 2,..., k}。对任意一对顶点i, j∈V,考虑从i到j且中间顶点皆属于集合{1, 2,..., k}的所有路径。设p是其中的一条最小权值路径。Floyd-Warshall算法利用了路径p与i到j之间的最短路径之间的联系。这一联系依赖于k是否是路径p的一个中间顶点:

a、 如果k不是路径p的中间顶点,则p的所有中间顶点皆在集合{1,2,...,k-1}中。因此从顶点i到顶点j且满足所有中间顶点皆属于集合{1,2,...,k-1}的一条最短路径,也同样是从i到j且满足所有中间顶点皆属于集合{1,2,...k}的一条最短路径。

b、 如果k是路径p的中间顶点,那么可将p分解为p1(i-->k)和p2(k-->j),因为顶点k不是路径p1上的一个中间顶点,所以p1是从i到k的一条最短路径,且其所有中间顶点皆属于集合{1,2,...,k-1}。类似的,p2以是从k到j的一条最短路径,且其所有中间顶点皆属于集合{1,2,...,k-1}

2、 根据上述讨论,我们用下式给出一个递归式:

因为对于任意路径,所有的中间顶点都在集合{1,2,...,n}内,矩阵D给出了最终解.

3、 基于递归式,下面的自底向上的过程按k值递增顺序计算最优解的值

Floyd-Warshall(W)
1 n<--rows[W]
2 D(0)<--W
3 for k<--1 to n
4 	do for i<--1 to n
5 		do for j<--1 to n
6 			do D(k,i,j)<--min(D(k-1, i, j), D(k-1, i, k) + D(k-1, k, j))
7 return D(n)

分析:Floyd-Warshall算法的运行时间是由第3-6行的三重嵌套for循环所决定的。每次执行第6行花费O(1)时间,因此算法的运行时间为Θ(n3)。算法中不包含复杂数据结构,因此隐含于Θ中的常 数很小。

4、 构造一个最优解:

在Floyd-Warshall算法中存在大量不同的方法来建立最短路径。一种途径是计算最短路径权值 的矩阵D,然后根据矩阵D构造前驱矩阵Π。这种方法可以在O(n^3)时间内实现。可以像Floyd-Warshall算法计算矩阵D(k)一样, 计算一个矩阵序列Π(0), Π(1),..., Π(n),其中Π=Π(n) ,并定义Pi(k, i, j)为从i出发的一条最短路径上顶点j的前趋,而这条路径上的中间顶点属于集合{1,
2,..., k}.

对于k=0:

对于k>= 1:

算法导论——lec 11 动态规划及应用

时间: 2024-10-24 02:46:32

算法导论——lec 11 动态规划及应用的相关文章

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

之前我们介绍了用动态规划的方法来解决一些最优化的问题.但对于有些最优化问题来说,用动态规划就是"高射炮打蚊子",采用一些更加简单有效的方法就可以解决.贪心算法就是其中之一.贪心算法是使所做的选择看起来是当前最佳的,期望通过所做的局部最优选择来产生一个全局最优解. 一. 活动选择问题 [问题]对几个互相竞争的活动进行调度:活动集合S = {a1, a2, ..., an},它们都要求以独占的方式使用某一公共资源(如教室),每个活动ai有一个开始时间si和结束时间fi ,且0 ≤ si &

算法导论三剑客之 动态规划 0-1背包问题

1 #include "iostream" 2 using namespace std; 3 4 float MAX(float m1,float m2){ 5 if(m1>=m2) 6 return m1; 7 else 8 return m2; 9 } 10 11 float bag_Zero_One(int n,float v,float p[],float w[]){ 12 if(n==0||v==0) 13 return 0; 14 else{ 15 float m2;

算法导论——lec 12 平摊分析与优先队列

在平摊分析中,执行一系列数据结构操作所需要的时间是通过对执行的所有操作求平均得出,反映在任何情况下(即最坏情况下),每个操作具有平均性能.掌握了平摊分析主要有三种方法,聚集分析.记账方法.势能方法.掌握了平摊分析的方法以后,我们就可以利用他来分析一些优先队列. 一. 平摊分析 [问题一]对作用于一个初始为空的栈上的n个PUSH.POP和MULTIPOP组成的序列进行分析. [问题二]考虑一个由0开始向上计数的k位二进制计数器.作用于一个初始为零的计数器上的n个INCREMENT操作的时间分析.

算法导论——lec 06 堆排序

堆数据结构是一种数组对象,它可以被视为一颗完全二叉树,树中每个节点和数组中存放该节点值的那个元 素对应.如果表示堆的数组为A,那么树的根为A[1]. 一. 堆 1. 表示堆的数组A是一个具有两个属性的对象:length(A)是数组中的元素个数,heap-size(A)是存放在A中的堆的 元素个数:A[heap-size(A)]之后的元素都不属于相应的堆.也就是:Heap-size(A)<=length(A). 2. 给定某个节点的下标i,其父节点PARENT(i),左儿子LEFT(i)和右儿子R

算法导论——lec 07 快速排序

一. 快速排序的描述 1. 快速排序是一种原地排序的算法,最坏情况下的时间复杂度为Θ(n^2),期望的运行时间为Θ(n logn),且其中隐含的常数因子较小. 2. 快速排序分三个步骤: 分解:数组A[p...r]被划分成两个数组A[p...q-1]和A[q+1...r],使得A[p...q-1]中的元素都小于等于A[q],A[q+1...r]中的元素都大于等于A[q].下标q在这个划分过程中计算. 解决:递归调用快速排序,对子数组A[p...q-1]和A[q+1...r]进行排序. 合并:两个

算法导论——lec 08 线性时间排序

之前我们介绍了几种O(nlgn)的排序算法:快速排序.合并排序和堆排序,本节我们介绍基于比较的排序算法的下界以及几个线性时间的排序算法--计数排序.基数排序.桶排序. 一. 比较排序算法的下界 1. 决策树模型:比较排序可以被抽象的视为决策树.一棵决策树是一棵满二叉树,表示某排序算法 作用于给定输入所做的所有比较. 排序算法的执行对应于遍历一条从树的根到叶结点的路径.在每个内节结点处要做比较. 要使排序算法能正确的工作,其必要条件是,n 个元素的n!种排列中的每一种都要作为决策树 的一个叶子而出

【算法导论】用动态规划解活动选择问题

上一篇讲了贪心算法来解活动选择问题([算法导论]贪心算法之活动选择问题),发现后面有一道练习16.1-1是要用动态规划来解活动选择问题.其实跟之前的矩阵链乘法有些相似,也是考虑分割的活动是哪一个,并用二维数据来记录Sij---最大兼容集合个数,和用另一个二维数据来记录Sij取得最大时的活动分割点k.然后就是考虑边界问题,和使用递归来求动态规划的最优解. 代码注解比较详尽: #include <iostream> #include <algorithm> using namespac

《算法导论》中动态规划求解钢条切割问题

动态规划算法概述 动态规划(dynamic programming)1是一种与分治方法很像的方法,都是通过组合子问题的解来求解原问题.不同之处在于,动态规划用于子问题重叠的情况,比如我们学过的斐波那契数列.在斐波那契数列的求解问题中,我们经常要对一个公共子问题进行多次求解,而动态规划算法,则对每个子问题只求解一次,将其解保存在一个表格中,从而避免了大量的冗余计算量. 动态规划算法常用于寻找最优解问题(optimization problem).而其规划大概可分为四步: 1.刻画一个最优解的结构特

算法导论 16.1-1

用动态规划方法求解活动选择问题,与贪心算法相比,显然是庞然大物,大材小用了,贪心算法可以非常简洁的求解活动选择问题 动态规划: 1 #include <iostream> 2 #include <vector> 3 #include <limits> 4 #define INF numeric_limits<int>::max() 5 using namespace std; 6 int act_select(int s[],int f[],int n,in