第六章 动态规划方法 §1 动态规划算法的基本思想 动态规划方法是处理分段过程最优化 问题的一类及其有效的方法。 在 实际生活中,有一类问题的活动过程可以分成若干个阶段,而且在任 一阶段后的行为依赖于该阶段的状态, 而与该阶段之前的过程如何达 到这种状态的方式无关。这类问题的解 决是多阶段的决策过程。在 50 年代,贝尔曼( Richard Bellman)等人提出了解决这类问题的“最 优化原理” ,从而创建了最优化问题的一种新的算法设计方法-动态 规划。 最优化原理: 多阶段过程的最优决策序列应当具有性质:无论过程的 初始状态和初始决策是什么, 其余的决策都必须相对于初始决策 所产生的状态构成一个最优决策序列。 对于一个多阶段过程问题, 上述最优决策是否存在依赖于该问题是否 有最优子结构性质: 原问题的最优解包含了其子问题的最优解。 而能 否采用动态规划的方法还要看该问题的子问题是否具有 重叠性质 。 后 面将会看到问题的 子结构性质 和 子问题重叠性质 是采用动态规划算 法的两个基本要素。先看两个例子: 例 1. 多段图问题 设 G=(V,E)是一个赋权有向图, 其顶点集 V 被划分成 k>2 个不相 交的子集 V i : 1≤i≤k,其中, V 1 和 V k 分别只有一个顶点 s(称为源 )和一 个顶点 t(称为汇 ), 图中所有的边 (u,v)的始点和终点都在相邻的两个子 集 V i 和 V i + 1 中: u∈V i , v∈V i + 1 。 V 1 V 2 V 3 V 4 V 5 4 6 9 4 4 2 5 7 2 S 7 3 2 t 3 1 11 5 2 5 11 6 8 一个 5 阶段的网络图 多阶段图问题: 求由 s 到 t 的最小成本路径 。 对于每一条由 s 到 t 的路径,可以把它看成在 k-2 个阶段作出的某个 决策序列的相应结果:第 i 步决策就是确定 V i+1 中那个顶点在这条路 径上。今假设 s, v 2 , v 3 , … , v k-1 , t 是一条由 s 到 t 的最短路径,再假定 从源点 s(初始状态 )开始,已经作出了到顶点 v 2 的决策 (初始决策 ),则 v 2 就是初始决策产生的状态。若将 v 2 看成是原问题的子问题的初始 状态,解这个子问题就是找一条由 v 2 到 t 的最短路径。事实上,这条 最短路径一定是 v 2 , v 3 , … , v k-1 , t。不然,设 v 2 , q 3 , … , q k-1 , t 是一条由 v 2 到 t 的比 v 2 , v 3 , … , v k-1 , t 更短的路径,则 s, v 2 , q 3 , … , q k-1 , t 是一条 由 s 到 t 的比 s, v 2 , v 3 , … , v k-1 , t 更短的路径。与前面的假设矛盾。这 说明多段图问题具有最优子结构性质。 例 2. 经典 0/1 背包问题 有 n 件物品,第 i 件重量和价值分别是 w i 和 p i , i=1, 2, …, n。要 将这 n 件物品的一部分装入容量为 c 的背包中, 要求每件物品或整个 1 2 4 6 9 10 11 12 3 7 8 5 装入或不装入,不许分割出一部分装入。 0/1 背包问题就是要给装包 算法,使得装入背包的物品的总价值最大。这个问题归结为数学规划 问题: ∑ ≤≤ ni ii xp 1 max s.t. nixcxw i ni ii ,,2,1},1,0{, 1 L=∈≤ ∑ ≤≤ ( 6.1) 0/1 背包问题具有最优子结构性质。 事实上, 若 n yyy ,,, 21 L是最优解, 则 n yy ,, 2 L将是是 0/1 背包问题的子问题 ∑ ≤≤ ni ii xp 2 max s.t. nixwcxw i ni ii ,,2},1,0{, 1 2 L=∈?≤ ∑ ≤≤ (6.2) 最优解。因为,若 n yy ',,' 2 L是子问题 (6.2)的最优解,且使得 ∑ ≤≤ ni ii yp 2 ' > ∑ ≤≤ ni ii yp 2 (6.3) 则 n yyy ',,', 21 L将是原问题 (6.1)的可行解,并且使得 ∑ ≤≤ + ni ii ypyp 2 11 ' > ∑ ≤≤ ni ii yp 1 (6.4) 这与 n yyy ,,, 21 L是最优解相悖。 例 3. 矩阵连乘问题 给定 n 个数字矩阵 A 1 , A 2 , …, A n ,其中 A i 与 A i+1 是可乘的, i=1,2,…,n-1.求矩阵连乘 A 1 A 2 ???A n 的加括号方法,使得所用的数乘次 数最少。 考察两个矩阵相成的情形: C=AB.如果矩阵 A, B 分别是 p× r 和 r× q 矩阵,则它们的乘积 C 将是 p× q 矩阵,其 (i, j)元素为 ∑ = = r k kjikij bac 1 (6.5) i=1,…,p, j=1,…,q, 因而 AB 所用的数乘次数是 prq。如果有至少 3 个 以上的矩阵连乘则涉及到乘积次序问题,即加括号方法。例如 3 个 矩阵连乘的加括号方法有两种: ((A 1 A 2 )A 3 )和 (A 1 (A 2 A 3 ))。设 A 1 , A 2 , A 3 分别是 p 0 × p 1 , p 1 × p 2 , p 2 × p 3 矩阵,则以上两种乘法次序所用的 数乘次数分别为: p 0 p 1 p 2 +p 0 p 2 p 3 和 p 0 p 1 p 3 +p 1 p 2 p 3 。如果 p 0 =10, p 1 =100, p 2 =5, p 3 =50, 则两种乘法所用的数乘次数分别为: 7500 和 750000。可 见,由于加括号的方法不同,使得连乘所用的数乘次数有很大差别。 对于 n 个矩阵的连乘积,令 P(n)记连乘积的完全加括号数,则有如下 递归关系 ? ? ? ? ? >? = = ∑ ? = 1 1 1)()( 11 )( n k nknPkP n nP (6.6) 由此不难算出 P=C(n-1),其中 C 表示 Catalan 数: )/4( 2 1 1 )( 2/3 n n n n nC n ?= ? ? ? ? ? ? + = (6.7) 也就是说, P(n)是随 n 指数增长的,所以,我们不能希望列举所有可 能次序的连乘积, 从中找到具有最少数乘次数的连乘积算法。 事实上, 矩阵连乘积问题具有最优子结构性质,我们可 以采用动态规划的方 法,在多项式时间内找到最优的连乘积次序。 用 A[i:j]表示连乘积 A i A i + 1 ???A j .分析计算 A[1:n]的一个最优次序。 设这个计算次序在矩阵 A k 和 A k+1 之间将矩阵连分开, 1≤k<n,则完 全加括号方式为 ((A 1 A 2 ???A k )( A k + 1 ???A n )),依此次序,我们先分别计算 A[1:k]和 A[k+1:n],然后将计算的结果相乘得到 A[1:n]。可见, A[1:n] 的一个最优序所包含的矩阵计算子链 A[1:k]和 A[k+1:n]的次序也一定 是最优的。也就是说,矩阵连乘问题具有最优子结构性质。 如上三个例子都具有最优子结构性质, 这个性质决定了解决此类 问题的基本思路是: 首先确定原问题的最优值和其子问题的最优值之 间的递推关系,然后自底向上递归地构造出最优解。最优子结构性质 是最优化原理得以采用的先决条件。一般说来,分阶段选择策略确定 最优解的问题往往会形成一个决策序列。 Bellman 认为,利用最优化 原理以及所获得的递推关系式去求解最优决策序列, 可以使枚举数量 急剧下降。这里有一个问题值得注意:最优子结构性质提示我们使用 最优化原理产生的算法是递归算法, 这很可能会增加时间与空间复杂 度。例如,用递归式直接计算矩阵连乘积 A[1:n] 的算法 RecurMatrixChain 的时间复杂度将是 )2( n ? : 计算矩阵连乘的递归算法 int RecurMatrixChain(int i, int, j, int p) { if (i==j) return 0; int u=RecurMatrixChain(i, i) +RecurMatrixChain(i+1,j) +p[i-1]*p[i]*p[j]; s[i][j]=i; for(int k=i+1; k<j; k++){ int t=RecurMatrixChain(i,k) +RecurMatrixChain(k+1,j) +p[i-1]*p[i]*p[j]; if (t<u) { u=t; s[i][j]=k;} } return u; } 如果用 T(n)表示该算法的计算 A[1:n]的时间,则有如下递归关系式: ? ? ? ? ? >+?++ = ≥ ∑ ? = 1 1 1)1)()((1 1)1( )( n k nknTkT nO nT 当 1>n 时 ∑∑∑ ? = ? = ? = +=++?+≥ 1 1 1 1 1 1 )(2)()()1(1)( n k n k n k kTnknTkTnnT - , 可用数学归纳法直接证明: )2(2)( 1 nn nT ?=≥ ? ,这显然不是我们所 期望的。 注意到,在用递归算法自顶向下求解具有最优子结构的问题时, 每次产生的子问题并不总是新问题,有些问题被反复计算多次。动态 规划算法正是利用了这种子问题的重叠 性质,对每一个问题只解一 次,而后将其解保存在一个表格中,当再次需要解此问题时,只是简 单地用常数时间查看一下结果。通常,不同的子问题个数随输入问题 的大小呈多项式增长。 因此, 用动态规划算法通常只需要多项式时间, 从而获得较高的效率。所以,可用动态规划算法求解得的问题应具备 的另一个基本要素是 子问题的重叠性 。 例如, 在矩阵的连乘积问题中, 若用 m[i][j]表示由第 i 个矩阵到第 j 个矩阵的连乘积所用的最少数乘 次数,则计算 m[1][n]时的子问题共有 )( 2 nΘ 个。这是因为,对于 nji ≤≤≤1 不同的有序对 (i, j)对应于不同的子问题, 不同子问题最多 只有 )( 2 2 nn n Θ=+ ? ? ? ? ? ? 个。用动态规划解此问题时,在计算过程中,保 存已解决的子问题答案,每个子问题只计算一次,而在后面用到时简 单地查一下,从而避免了大量的重复计算。 求矩阵连乘最优次序的动态规划算法 void MatrixChain(int p, int n, int * *m, int * *s) { for (int i=1; i<=n; i++) m[i][i]=0; for (int r=2; r<=n; r++){ for (int i=1; i<=n-r+1; i++) int j=i+r-1; m[i][j]= m[i+1][j]+p[i-1]*p[i]*p[j]; s[i][j]=i; for (int k=i+1; k<j; k++){ int t= m[i][k]+ m[k+1][j]+p[i-1]*p[k]*p[j]; if (t< m[i][j]) { m[i][j]=t; s[i][j]=k; } } } } 算法 MatrixChain 的主要计算量取决于程序中对 r, i 和 k 的三重循环, 循环体内的计算量为 O(1),三重循环的总次数是 O(n 3 ),所以,算法 的计算时间上界为 O(n 3 )。 注意, 上述算法只是明确给出了矩阵最优连乘次序所用的数乘次 数 m[1][n],并未明确给出最优连乘次序,即完全加括号方法。但是 以 s[i][j]为元素的 2 维数组却给出了足够的信息。事实上, s[i][j]=k 说明,计算连乘积 A[i:j]的最佳方式应该在矩阵 A k 和 A k+1 之间断开, 即最优加括号方式为 (A[i:k])(A[k+1:j])。下面的算法 Traceback 按算法 MatrixChain 计算出的断点信息 s 指示的加括号方式输出计算 A[i:j]的 最优次序。 根据最优值算法构造最优解 Void Traceback(int i, int j, int * * s) { if (i==j) return; Traceback(i, s[i][j], s); Traceback(s[i][j]+1, j, s); cout << “Multiply A” << i << “,” << s[i][j]; cout << “and A” << (s[i][j] +1) << “,” << j; << endl; } 总结上面解矩阵连乘积问题,我们 可以归纳出使用动态规划算法 的基本步骤: 1. 分析最优解的结构 在这一步中,应该确定要解决的问题 应该是具有最小子结构性质, 这是选择动态规划算法的基础。 2. 建立递归关系 第一步的结构分析已经为建立递归关系奠定了基础。 这一步的主要 任务是递归地定义最优值, 并建立该问题与其子问题最优值之间的 递归关系。例如在矩阵连乘积问题中,递归关系为 {} ? ? ? <+++ = = ≤≤ ji]]*p[k]*p[jp[i-] [j] m[km[i] [k] ji jim jki 11min 0 ]][[ 在经典 0/1 规划问题中的递归关系是 { } 1111 )(),(max)( ++++ +?= jjjj pwXgXgXg (6.8) 在多段图问题中的递归关系是 { }),1(),(min),( ),(, 1 liCOSTljcjiCOST EliVl i ++= ∈∈ + (6.9) 这里 j 表示取 V i 中的顶点 j。 3. 计算最优值 依据递归关系式可以自底向上的方式或自顶向下的方式进行计算, 在计算过程中保存已经解决的子问题答案。每个子问题只计算一次, 而在后面需要时只要简单查一下,从而避免大量的重复计算,最终获 得多项式时间的算法。 4. 造最优解 依据求最优值时记录的信息,构造出最优解。 上述归纳的 4 个阶段是一个整体,必要时才分步完成,很多时是统 一来完成的。 §2 多段图问题 多段图是一种简单而经典的模型, 它既能地反映了动态规划算法的基 本特征,而且在实际问题中有较多的应用。 V 1 V 2 V 3 V 4 V 5 7 4 4 6 9 7 4 4 9 2 5 7 2 S 7 5 3 2 2 t 16 3 18 1 11 5 2 7 5 15 11 6 5 8 由后向前的处理方法(动态规划方法) V 1 V 2 V 3 V 4 V 5 9 13 4 6 9 9 4 4 7 2 5 7 2 S 7 11 3 14 2 t 3 3 1 16 11 5 2 10 5 2 11 6 16 8 由前向后的处理方法(备忘录方法) 根据 (6.9)式,我们可以由后向前逐步确定各阶段中的顶点到汇顶点 t 的最短路径。对于如上的 5 阶段网络图,上图中的红色字体标出了各 顶点到汇顶点 t 的最短距离。 事实上,我们也可以由前向后逐步确定由源顶点 s 到各阶段中顶 1 2 4 6 9 10 11 12 3 7 8 5 1 2 4 6 9 10 11 12 3 7 8 5 点的最短路径,此时的递归关系为 { }),(),1(min),( ),(, 1 jlcliCOSTjiBCOST EjlVl i +?= ∈∈ ? (6.10) 多段图的动态规划算法伪代码 MultiGraph(E, k, n, P) //有 n 个顶点的 k 段图 G(按段序统一 //编号) , E 是边集, c(I, j)是边 (I, j)的成本, P[1:k]是最小成 //本路径。 1 real COST(n); integer D(n-1), P(k), r, j, k, n; 2 COST(n)=0; 3 for j from n-1 by –1 to 1 do 4 设 r 是这样一个顶点, (j, r)∈E 且使得 c(j, r)+COST(r)取最 小值 5 COST(j)= c(j, r)+COST(r); 6 D(j)=r; 7 endfor 8 P(1)=1; P(k)=n; 9 for j from 2 to k-1 do 10 P(j)=D(P(j-1)); 11 endfor 12 end MultiGraph 这里, D(j)将由 j 到汇顶点 t 的最短路径上 j 后面的顶点记录下来, P(j) 则记录由源顶点 s 到汇顶点 t 的最短路径上处于第 j 阶段中的顶点。 语句 10 是根据数组 D 中的信息逐段寻找到由源顶点 s 到汇顶点 t 的 最短路径上的各个顶点。如果用邻接表表示图 G,则语句 4 中 r 的确 定可以在与 d + (j)成正比的时间内内完成。因此,语句 3- 7 的 for 循 环所需的时间是 |)|( En+Θ 。循环体 9- 11 所需时间是 nk ≤Θ )( ,因 而算法 MultiGraph 的时间复杂度是 |)|( En+Θ 。 下面给出的备忘录算 法的时间复杂度也是若此。 多段图的备忘录算法伪代码 MemorizedMultiGraph(E, k, n, P) //有 n 个顶点的 k 段图 G(按 //段序统一编号) , E 是边集, c(I, j)是边 (I, j)的成本, P[1:k] //是最小成本路径。 1 real BCOST(n); integer D(n-1), P(k), r, j, k, n; 2 BCOST(1)=0; 3 for j from 2 to n do 4 设 r 是这样一个顶点, (r, j)∈E 且使得 BCOST(r)+ c(r, j) 取最小值 5 BCOST(j)= BCOST(r)+ c(r, j); 6 D(j)=r; 7 endfor 8 P(1)=1; P(k)=n; 9 for j from k-1 by –1 to 2 do 10 P(j)=D(P(j+1)); 11 endfor 12 end MemorizedMultiGraph § 3 0/1 背包问题 本节将使用动态规划的由前向后处理方法(也称备忘录算法)处理 0/1 背包问题: ∑ ≤≤ ni ii xp 1 max s.t. nixcxw i ni ii ,,2,1},1,0{, 1 L=∈≤ ∑ ≤≤ ( 6.1) 通过作出变量 x的取值序列 n xxx ,,, 21 L来给出最优解。 这个取值序列 的对应的决策序列是 11 ,,, xxx nn L ? 。在对 n x 作出决策之后,问题 处于下列两种状态之一:背包剩余的容量仍为 c,此时未产生任何效 益;背包的剩余容量为 n wc ? ,此时的效益值增长了 n p 。因而 }]][1[],][1[max{]][[ nn pwcnmcnmcnm +???= (6.11) 一般地,如果记 0/1 背包子问题问题: ∑ ≤≤ ki ii xp 1 max s.t. kixXxw i ki ii ,,2,1},1,0{, 1 L=∈≤ ∑ ≤≤ (6.12) 最优解的值为 ]][[ Xkm ,则有 }]][1[],][1[max{]][[ kk pwXkmXkmXkm +???= (6.13) 这里 cX ≤≤0 ,而 且 nk ,,2L= 。为 使 (6.13)式能够有效地递推下去, 需要延拓 k 和 X 的取值范围: ),(;,2,1 +∞?∞∈= XnkL;补充定义: nkkm Xif Xif XmL,2,1,]0][[ ;00 ;0 ]][0[ =?∞=< ? ? ? ≥ <∞? = (6.14) 事实上,我们主要目的是计算出 ]][[ cnm ,根据 (6.11)式,我们 可能需要计算 ]][1[ n wcnm ?? ,而为了计算 ]][1[ n wcnm ?? ,可能需 要计算 ]][2[ 1 nn wwcnm ??? ? ,如此等等,在递推公式 (6.13)中用到 的 X 值可能为负数。另外,如果将 ]][[ Xkm 看作 X 的函数,则是一 个分段函数,而且当 ∑ ≤≤ ≥ ki i wX 1 时取常值。所以将 X 的取值范围延拓 至全体实数。 例子 5),5,2,1(),,(),4,3,2(),,(,3 321321 ==== cwwwwwwn 。 33 22 11 ]][2[ 98}53,2max{ 977}52,3max{ 766}51,3max{ 655}50,3max{ 545}50,2max{ 432 321 200 0 ]][2[ ]][1[ 53}21,1max{ 532}20,1max{ 321 200 0 ]][2[ ]][0[ 21}10,0max{ 200 0 ]][1[ , 00 0 ]][0[ pwXm X X X X X X X X X Xm pwXm X X X X X Xm pwXm X X X Xm X X Xm +? ? ? ? ? ? ? ? ? ? ? ? ? ? ≤=+ <≤=+ <≤=+ <≤=+ <≤=+ <≤ <≤ <≤ <∞? = +? ? ? ? ? ? ? ? ≥=+ <≤=+ <≤ <≤ <∞? = +? ? ? ? ? ? ≥=+ <≤ <∞? = ? ? ? ≥ <∞? = 由递推式 (6.13), ]][[]][1[ XkmXkm ≤? ,而且当 k wX < 时等式成立,这 一事实可以写成 nk wXpwXkmXkm wXXkm Xkm kkk k ,,2,1 },]][1[],][1[max{ ],][1[ ]][[ L= ? ? ? ≥+??? <? = 上述诸函数 ]][[ Xkm 具有如下性质: 1.每个阶梯函数 ]][[ Xkm 都是由其跳跃点偶 ])][[,( bkmb 决定的。如 果有 1+r 个跳跃点 : r bbb <<<L 10 ,各点的函数值分别是 r vvv ,,, 10 L,则 ribXbifvXkm iii ,,1,0,]][[ 1 L=<≤= + (6.15) 这里约定 +∞= +1r b 。 2.令 S k 是阶梯函数 ]][[ Xkm 的跳跃点偶的集合,则阶梯函数 kk pwXkm +?? ]][1[ 的跳跃点偶之集去掉点偶 (0,0)后,恰是集合 i a S ={ } 1 ),(|),( ? ∈?? k kk Spvwbvb (6.16) 这是因为函数 kk pwXkm +?? ]][1[ 的图象恰是由函数 ]][1[ Xkm ? 的图象先向右平移 k w ,然后再向上移动 k p 而得。如前面例子 m[1][X-w 2 ]+p 2 3 3 2 m[1][X] 2 1 1 b b 0 1 2 3 4 5 6 0 1 2 3 4 5 6 7 S 1 ={(0,0), (2,1)} (w 2 , p 2 )=(3, 2) S 2 a ={ (3,2), (5,3)} m[2][X] 3 2 1 S 2 ={(0,0), (2,1), (3,2), (5,3)} 0 1 2 3 4 5 6 7 b 根据递推公式 (6.13), S k 是从 S k-1 ∪ S k a 中去掉那些点偶 ),( ii vb : 在 S k-1 ∪ S k a 中存在点偶 ),( kk vb 使得 ki bb ≥ 而且 ki vv ≤ , 此时我们说 ),( kk vb 淹 没 ),( ii vb 。前面例子的诸 S k 计算如下: )}.8,9(),7,7(),6,6(),5,4(),2,3(),1,2(),0,0{( )}8,9(),7,7(),6,6(),5,4{(),( ),5,4(),()};3,5(),2,3(),1,2(),0,0{( )}3,5(),2,3{(),(),2,3(),()};1,2(),0,0{( )}1,2{(),(),1,2(),()};0,0{( 3 2 33 3 33 2 1 22 2 22 1 0 11 1 11 0 = =+= == =+=== =+=== S SpwS pwS SpwSpwS SpwSpwS a a a 在由 2 S 、 3 a S 向 3 S 的归并过程中, (5, 3)被 (4, 5)淹没。 3.在处理实际问题时,由于要求 cX ≤ ,在计算 1 ),( ? += k kk k a SpwS 时应该去掉那些使得 cb > 的跳跃点偶 ),( vb 。根 据前面的提到的淹没规则,如果将 k S 中的元素按照第一个分量的递 增次序排列,则第二个分量也呈递增的次序,而且 n S 的最后一个元 素,设为 ),( vb ,的 v值即是 0/1 背包问题 (6.1)的最优值 ]][[ cnm 。 4.最优解的获得可以通过检查诸 k S 来确定。设 ),( nn kk vb 是 n S 的 最后一个元素。 若 1 ),( ? ∈ n kk Svb nn , 则 0= n x . 因为此时函数 ]][[ Xnm 和函数 ]][1[ Xnm ? 的最大值一致,表明 0/1 背包问题 (6.1)与其子问题 ∑ ?≤≤ 11 max ni ii xp s.t. 1,,2,1},1,0{, 11 ?=∈?≤ ∑ ?≤≤ nixwcxw in ni ii L (6.17) 有相同的最优值。若 1 ),( ? ? n kk Svb nn ,则 1= n x 。因为,此时函数 ]][[ Xnm 的最大值是函数 ]][1[ Xnm ? 的最大值加 n p ,相应地, 0/1 背 包问题 (6.1)的最优值是其子问题 (6.17)的最优值加 n p 。注意到此时跳 跃点偶一定具有形式 ),(),(),( 11 ?? += nnnn kknnkk vbpwvb (6.18) 其中 1 ),( 11 ? ∈ ?? n kk Svb nn 。于是,我们可以依 2 ),( 11 ? ∈ ?? n kk Svb nn 与否而 决定 1?n x 取 0 或 1。 如此等等, 我们可以逐一确定 11 ,,, xxx nn L ? 的值。 在上述例子中,整理后的诸 k S 为: )}.6,6(),5,4(),2,3(),1,2(),0,0{( )};3,5(),2,3(),1,2(),0,0{( )};1,2(),0,0{( )};0,0{( 3 2 1 0 = = = = S S S S (6, 6)是 3 S 的最后一个跳跃点偶,所以该 0/1 问题的最优值是 6。这个 点偶不在 2 S 中,因而 1 3 =x ; 又 2 )1,2()5,4()6,6( S∈=? ,据此判断 2 x 的取值。因为 1 )1,2( S∈ ,所以 0 2 =x ;最后由 0 )1,2( S? 知 1 1 =x , 所以最优解为 )1,0,1( . 为实现上述算法,可以用两个一维数组 B 和 V 来存放所有的跳 跃点偶 (b, v),跳跃点集 110 ,,, ?n SSSL互相邻接地存放,并指针 F(k) 来指示集合 S k ,即 S k 的第一个元素的位置(这里不存放 S n 是由于我 们只需要它的最后一个元素, 而这个元素或者是 S n-1 的最后一个元素, 此时 S n-1 与 S n 有相同的最后元素;或者具有形式 (6.18),而且 1?n k v 是 满足 cwv nk n ≤+ ?1 的最大值。所以,确定 S n 的最后元素不必将 S n 的 元素全部求出来。而且 确定最优解的其它变量 11 ,, xx n L ? 时也不使用 数据 S n ) 。用 F(n)指示 S n-1 的最后一个元素的位置。因为产生 S k 仅使 用信息 S k-1 和 ),( kk pw ,所以不必存储 k a S 。根据以上分析,我们不难 给出动态规划解 0/1 背包问题的算法伪代码。 0/1背包问题动态规划算法伪代码 DKnapsack(w, p, c, n, m) //数组 w, p 中的元素分别代表各件物品的 //重量和价值, n 是物品个数, c 代表背包容量 real p(n), w(n), B(m), V(m), ww, pp, c; integer F[0:n], l, h, i, j, p, next; F[0]=1; B[0]=V[0]=0; l=1; h=1; //S 0 的首和尾 F(1)=2; next=2; // B、 V 中的第一个空位 for i from 1 to n-1 do k=1; u=最大指标 r,使得 1 ≤ r ≤ h, 而且 B[r]+w i ≤ c; for j from 1 to u do (ww, pp)=( B[r]+w i , V[r]+p i ); //S i a 中的下一个元素 while k ≤ h && B[k] < ww do //从 S i-1 中取来元素归并 B[next]=B[k]; V[next]=V[k]; Next=next+1; k=k+1; endwhile if k ≤ h && B[k]=ww then pp=max(B[k], pp); k=k+1; endif if pp > V[next-1] then (B[next], V[next])=(ww, pp); next=next+1; endif while k ≤ h && V[k] < V[next-1] do //清除 k=k+1; endwhile endfor while k ≤ h do // 将 S i-1 剩余的元素并入 S i (B[next], V[next])= (B[k], V[k]); next=next+1; k=k+1; endwhile l=h+1; h=next-1; F(I+1)=next; //为 S i+1 赋初值 endfor traceparts // 逐一确定 11 ,,, xxx nn L ? end Dknapsack 算法 DKnapsack 的主要工作是产生诸 S i 。在 i > 0 的情况下,每 个 S i 由 S i-1 和 S i a 归并而成,并且 |S i a | ≤ |S i-1 |。因此 |S i | ≤ 2|S i-1 |, 122|| 1111 ?== ∑∑ ?≤≤?≤≤ n ni i ni i S 由此知,算法 DKnapsack 的空间复杂度是 )2( n O 。 由 S i-1 生成 S i 需要 |)(| 1? Θ i S 的时间, 因此, 计算 110 ,,, ?n SSSL总 共需要的时间为 122|| 1 11 1 11 1 ?=≤ ? ?≤≤ ? ?≤≤ ? ∑∑ n ni i ni i S ,算法 DKnapsack 的 时间复杂度为 )2( n O 。 如果物品的重量 i w 和所产生的效益值 i p 都是整数,那么, i S 中 的元素 ),( vb 的分量 b 和 v 也都是整数,且 ∑ ≤≤ ≤≤ ik k pvcb 1 , 。又 i S 中 不同的元素对应的分量也都是不同的,故 ? ? ? ? ? ? +≤ +≤ ∑ ∑ ≤≤ ≤≤ cwS pS ik k i ik k i ,min1|| 1|| 1 1 此时算法 DKnapsack 的时间复杂度为 ),,2(min 1 ? ? ? ? ? ? ∑ = n i k n pnncO 。 §4 流水作业调度问题 问题描述 :已知 n 个作业 {1, 2, . . . , n}要在由两台机器 M 1 和 M 2 组成 的流水线上完成加工。每个作业加工的顺序都是先在 M 1 上加工,然 后在 M 2 上加工。 M 1 和 M 2 加工作业 i 所需的时间分别为 a i 和 b i , 1≤ i ≤ n。流水作业调度问题要求确定这 n 个作业的最优加工次序,使得 从第一个作业在机器 M 1 上开始加工,到最后一个作业在机器 M 2 上 加工完成所需的时间最少。 记 N={1, 2, . . . , n}, S 为 N 的子集合。一般情况下,当机器 M 1 开始加工 S 中的作业时,机器 M 2 可能正在加工其它的作业,要等待 时间 t 后才可用来加工 S 中的作业。将这种情况下流水线完成 S 中的 作业所需的最短时间记为 T(S, t),则流水作业调度问题的最优值即是 T(N, 0)。 流水作业调度问题具有最优子结构性质 。事实上,设 π 是 n 个流 水作业的一个最优调度(实际上是作业的一个排序) ,其所需要的加 工时间为 ' )1( Ta + π ,其中, 'T 是在机器 M 2 的等待时间为 )1(π b 时,安 排作业 )(,),2( nππL所需的时间。若记 )}1({\ πNS = ,我们证明 ),(' )1(π bSTT = 。 首先由 'T 的定义知 ),(' )1(π bSTT ≥ 。如果 ),(' )1(π bSTT > ,设 'π 是 作业集 S 在机器 M 2 等等时间为 )1(π b 情况下的一个最优调度,则 )(',),2('),1( nπππL是 N 的一个调度,该调度所需的时间为 '),( )1()1()1( TabSTa +<+ πππ ,这与 π 是 N 一个最优调度矛盾。所以 ),(' )1(π bSTT = ,说明流水作业调度问题具有最优子结构性质。 关于流水作业调度问题的最优值递推关系式, 我们可以如下建立。 容易看出 )}},{\({min)0,( 1 ii ni biNTaNT += ≤≤ (6.19) 上述关系式可以推广到一般情形: })}0,max{},{\({min),( iii Si atbiSTatST ?++= ∈ (6.20) 其中, }0,max{ i at ? 一项是由于在机器 M 2 上, 作业 i 必须在 },max{ i at 时间之后才能开工。因此,在机器 M 1 完成作业 i 之后,机器 M 2 还需 等待 }0,max{},max{ iiiii atbaatb ?+=?+ 时间后才能完成对作业 i 的加工。 按照递归关系 (6.20),我们容易给出流水调度问题的动态规划算 法, 但是其时间复杂度将是 )2( n O 。 以下我们将根据这一问题的特点, 采用 Johnson 法则简化算法,使得到时间复杂度为 )log( nnO 。 设 π 是作业集 S 在机器 M 2 的等待时间为 t 时的任一最优调度。若 在这个调度中,安排在最前面的两个作业分别是 ji和 ,即 ji == )2(,)1( ππ 。则由递归关系式 (6.20)的 )},,{\(})0,max{},{\(),( ijjiiii tjiSTaaatbiSTatST ++=?++= 其中 },,max{ }0,,max{ }},0,max{max{ }0,}0,max{max{ iijiijij ijijij ijijij jiijij abaataabb baatabb baatabb aatbbt ?++??+= ??+?+= ??+?+= ??++= 如果作业 ji和 满足 },min{},min{ ijji baba ≤ (6.21) 则称作业 ji和 满足 Johnson 不等式;如果作业 ji和 不满足 Johnson 不 等式,则交换作业 ji和 的加工次序使满足 Johnson 不等式。在作业集 S 当机器 M 2 的等待时间为 t 时的调度 π 中, 交换作业 ji和 的加工次序, 得到作业集 S 的另一个调度 'π ,它所需的加工时间为 )},,{\(),(' jiji tjiSTaatST ++= 其中, },,max{ jjjiijijij abaataabbt ?++??+= 。当作业 ji和 满 足 Johnson 不等式时 (6.21)时,有 },max{},max{ ijji baba ??≥?? 从而 },max{},max{ ijjijiji baaabaaa ??++≥??++ 由此可得 },max{},max{ iijijjji abaaabaa ?+≥?+ 。因此,对于任 意 t 有 },,max{},,max{ iijijjji abaatabaat ?+≥?+ ,即 ijji tt ≥ 。 可见 ),(),(' tSTtST ≥ 。换句话说,当作业 ji和 不满足 Johnson 不等式 时,交换它们的加工次序后,作业 ji和 将满足 Johnson 不等式,而且 不会增加加工时间。由此可知,对于流水作业调度问题,必然存在一 个最优调度 π ,使得作业 )1()( +ii ππ 和 满足 Johnson 不等式: 11},,min{},min{ )()1()1()( ?≤≤≤ ++ nibaba iiii ππππ 一般地,可以证明,上述不等式与不等式 njibaba ijji ≤<≤≤ 1},,min{},min{ )()()()( ππππ (6.22) 等价。 以下给出的是流水作业调度问题的 Johnson 算法: (1) 令 }|{},|{ iiii abiBAbaiAB ≤=<= ; (2) 将 AB中作业依 i a 的非减次序排列;将 BA中作业依 i b 的非增 次序排列; (3) AB中作业接 BA中作业即构成满足 Johnson 法则的最优调 度。 流水作业调度问题的 Johnson 算法 int FlowShop(int n, int a, int b, int c) { class Jobtype{ public: int operator <= (Jobtype a) const {return (key <= a.key);} int key; int index; bool job; }; Jobtype *d=new Jobtype [n]; for (int i = 0; i< n; i++) { D[i].key = a[i] > b[i] ? b[i] : a[i]; D[i].job = a[i] <= b[i]; D[i].index = i; } sort(d, n) int j = 0, k = n-1; for (int i = 0; i < n; i++) { if ( d[i].job ) c[j++] = d[i].index; else c[k--] = d[i].index; } j = a[c[0]]; k = j + b[c[0]]; for (int i = 1; i < n; i++) { j + =a[c[i]]; k = j < k ? k + b[c[i]] : j + b[c[i]]; } delete d; return k; } 上述算法的时间主要花在排序上,因此,在最坏情况下的时间复 杂度为 )log( nnO ,空间复杂度为 )(nO 更容易看出。 §5 最优二叉搜索树 设 },,,{ 21 n xxxSL= 是一个有序集合, 且 n xxx <<<L 21 .表示有序 集合的二叉 搜索树利用二叉树的顶点存储有序集中的元素,而且具 有性质: 存储于每个顶点中的元素 x 大于其左子树中任一个顶点中存 储的元素,小于其右子树中任意顶点中存储的元素。二叉树中的叶顶 点是形如 ),( 1+ii xx 的开区间。在二叉搜索树中搜索一个元素 x,返回 的结果或是在二叉树的内部顶点处找到: i xx = ; 或是在二叉树的叶 顶点中确定: ),( 1+ ∈ ii xxx .现在假定在第一种情况出现,即 i xx = 的 概率为 i b ;在第二种情况出现,即 ),( 1+ ∈ ii xxx 的概率为 i a .这里约定 +∞=?∞= +10 , n xx . 显然 1;1,0;0,0 10 =+≤≤≥≤≤≥ ∑∑ == n j j n i iji banjbnia (6.23) 称集合 },,,,,{ 110 nn ababaL为有序集 S 的存取概率分布。 在一个表示 S 的二叉搜索树 T 种, 设存储元素 i x 的顶点深度 (从 根到该顶点的距离)为 i c ,叶顶点 ),( 1+ii xx 的深度为 i d ,则 ∑∑ == ++= n j jj n i ii dacbp 01 )1( (6.24) 表示在二叉搜索树 T 中做一次搜索所需的平均比较次数。 p也称为二 叉搜索树 T 的平均路长。 最优二叉搜索树问题对于有序集 S 及其存取概率分布 },,,,,{ 110 nn ababaL,在所有表示 S 的二叉搜索树中,找出一棵具有 最小平均路长的二叉搜索树。 为了采用动态规划算法, 我们首先要考虑该问题是否具有最优子 结构性质。二叉搜索树 T 的一棵含有顶点 jii xxx ,,, 1 L + 和叶顶点 ),(,),,(),,( 111 ++? jjiiii xxxxxxL的子树 T 可以看作是有序集 },,,{ 1 jii xxxL + 关于全集为实数区间 ),( 11 +? ji xx 的一棵二叉搜索树 ( T 自身可以看作是有序集 },,,{ 21 n xxxSL= 关于全集为整个实数 区间 ),( +∞?∞ 的子树) 。根据 S 的存取分布概率, x 在子树 T 的顶点 处被搜索到的概率是 ∑∑ ≤≤≤≤? += jki k jki kij baw 1 (6.25) 于是 },,,{ 1 jii xxxL + 的存储概率分布为 },,,,{ 1 jjii abbaL ? ,其中, kh ba , 分别是下面的条件概率: jhiwaajkiwbb ijhhijkk ≤≤?=≤≤= 1,;, (6.26) 设 ij T 是有序集 },,,{ 1 jii xxxL + 关于存储概率分布 },,,,{ 1 jjii abbaL ? 的一棵最优二叉搜索树,其平均路长记为 ij p . ij T 的根顶点存储的元 素是 m x ,其左子树 l T 和右子树 r T 的平均路长分别记为 l p 和 r p .由于 l T 和 r T 中顶点深度是它们在 ij T 中的深度减 1,我们得 rjmlmiijijij pwpwwpw ,11, +? ++= (6.27) 由于 l T 是有序集 },,{ 1?mi xxL的一棵二叉搜索树,故 1, ? ≥ mil pp .若 1, ? > mil pp , 则 1, ?mi T 替换 l T 可得到平均路长比 ij T 更小的二叉搜索树。 这与 ij T 是最优二叉搜索树矛盾。同理可证, r T 也是一棵最优二叉搜 索树。因此,最优二叉搜索树问题具有最优子结构性质。 x 6 T l T r T 2,10 采用上面的记号,则 n 元最优二叉搜索树问题即是求 T 1 , n ,其最 优值为 p 1,n 。由最优儿茶搜索树的最优子结构性质,可以建立最优值 p i,j 的递推公式: jipwpwwpw jkjkkiki jki ijijij ≤++= ++?? ≤≤ },{min ,1,11,1, (6.28) 初始时, nip ii ≤≤= ? 1,0 1, . 记 ijij pw 为 ),( jim ,则 nnn ppwnm ,1,1,1 ),1( == 为所求最优值。由 (6.28)不难推得关于 ),( jim 的递推公式 niiim jijkmkimwjim jki ij ,,2,1,0)1,( )},,1()1,({min),( L==? ≤++?+= ≤≤ (6.29) 据此,可以设计求解最优二叉搜索树问题的动态规划算法。 最优二叉搜索树的动态规划算法 void OptimalBinarySearchTree( int a, int b, int n, int **m, int **s, int **w) { for (int i = 0; i < n; i++) { w[i+1][i] = a[i]; x 4 x 3 x 5- x 2 D 1 D 2 D 3 D 4 D 5 x 8 x 9 x 7 x 10 D 10 D 8 D 7 D 6 D 9 m[i+1][i] = 0; } for (int r = 0; ir< n; r++) { for (int i = 1; i <= n-r; i++) { int j = i+r; w[i][j] = w[i][j-1] + a[j] +b[j]; m[i][j] = m[i+1][j]; s[i][j] = i; for (int k =i + 1; k<= j; k++) { int t = m[i][k-1] + m[k+1][j]; if (t < m[i][j]) { m[i][j] = t; s[i][j] = k; } } m[i][j]+=w[i][j]; } } } 算法 OptimalBinarySearchTree 中用 s[I][j]保存最优子树的根顶 点中元素。当 s[1][n] = k 时, x k 为所求二叉搜索树的根顶点元素。其 左子树为 T(1,k-1),因此 I = s[1][k-1]即表示 T(1,k-1)的根顶点为 x i 。 依次类推, 容易由 s 记录的信息在 O(n)时间内构造出所求的最优二叉 搜索树。算法中用到三个数组 m、 s、 w,故所需的空间为 O(n 2 ).算法 的主要计算量在于计算 )},1()1,({min jkmkimw jki ij ++?+ ≤≤ 。 对于固定 的差 r=j-I,需要计算时间 )1()1( +=+? rOijO 。因此算法所耗费的 总时间为 ∑∑ ? = ? = =+ 1 01 3 )()1( n r rn i nOrO .