假设n =8, [w1 , ... w8 ]=[100,200,50,90,150,50,20,80], c= 4 0 0。利用贪婪算法时,所考察货箱的顺序为7 , 3 , 6 , 8 , 4 , 1 , 5 , 2。货箱7 , 3 , 6 , 8 , 4 , 1的总重量为3 9 0个单位且已被装载,剩下的装载能力为1 0个单位,小于剩下的任何一个货箱。在这种贪婪解决算法中得到[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 + +代码实现见程序1 3 - 1。由于贪婪算法按货箱重量递增的顺序装载,程序1 3 - 1首先利用间接寻址排序函数I n d i r e c t S o r t对货箱重量进行排序(见3 . 5节间接寻址的定义),随后货箱便可按重量递增的顺序装载。由于间接寻址排序所需的时间为O (nl o gn)(也可利用9 . 5 . 1节的堆排序及第2章的归并排序),算法其余部分所需时间为O (n),因此程序1 3 - 1的总的复杂性为O (nl o gn)。
程序13-1 货箱装船
template <class T>
void ContainerLoading(int x[], T w[], T c, int n)
{// 货箱装船问题的贪婪算法
// x[i] = 1 当且仅当货箱i被装载, 1 <=i <=n
// c是船的容量, w 是货箱的重量
// 对重量按间接寻址方式排序
// t 是间接寻址表
int *t = new int [n+1];
I n d i r e c t S o r t ( w, t, n);
// 此时, w[t[i]] <= w[t[i+1]], 1 <=i <n
// 初始化x
for (int i = 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.2 0/1背包问题
在0 / 1背包问题中,需对容量为c 的背包进行装载。从n 个物品中选取装入背包的物品,每件物品i 的重量为wi ,价值为pi 。对于可行的背包装载,背包中物品的总重量不能超过背包的容量,最佳装载是指所装入的物品价值最高,即n åi=1pi xi 取得最大值。约束条件为n åi =1wi xi≤c 和xiÎ[ 0 , 1 ] ( 1≤i≤n)。
在这个表达式中,需求出xt 的值。xi = 1表示物品i 装入背包中,xi =0 表示物品i 不装入背包。0 / 1背包问题是一个一般化的货箱装载问题,即每个货箱所获得的价值不同。货箱装载问题转化为背包问题的形式为:船作为背包,货箱作为可装入背包的物品。
动态规划法
例3-2 [0/1背包问题] 考察1 3 . 4节的0 / 1背包问题。如前所述,在该问题中需要决定x1 .. xn的值。假设按i = 1,2,.,n 的次序来确定xi 的值。如果置x1 = 0,则问题转变为相对于其余物品(即物品2,3,.,n),背包容量仍为c 的背包问题。若置x1 = 1,问题就变为关于最大背包容量为c-w1 的问题。现设rÎ{c,c-w1 } 为剩余的背包容量。
在第一次决策之后,剩下的问题便是考虑背包容量为r 时的决策。不管x1 是0或是1,[x2 ,.,xn ] 必须是第一次决策之后的一个最优方案,如果不是,则会有一个更好的方案[y2,.,yn ],因而[x1,y2,.,yn ]是一个更好的方案。
假设n=3, w=[100,14,10], p=[20,18,15], c= 11 6。若设x1 = 1,则在本次决策之后,可用的背包容量为r= 116-100=16 。[x2,x3 ]=[0,1] 符合容量限制的条件,所得值为1 5,但因为[x2,x3 ]= [1,0] 同样符合容量条件且所得值为1 8,因此[x2,x3 ] = [ 0,1] 并非最优策略。即x= [ 1,0,1] 可改进为x= [ 1,1,0 ]。若设x1 = 0,则对于剩下的两种物品而言,容量限制条件为11 6。总之,如果子问题的结果[x2,x3 ]不是剩余情况下的一个最优解,则[x1,x2,x3 ]也不会是总体的最优解。
例3-4 [0/1背包] 在例3 - 2的0 / 1背包问题中,最优决策序列由最优决策子序列组成。假设f (i,y) 表示例1 5 - 2中剩余容量为y,剩余物品为i,i + 1,.,n 时的最优解的值,即:和利用最优序列由最优子序列构成的结论,可得到f 的递归式。f ( 1 ,c) 是初始时背包问题的最优解。可使用( 1 5 - 2)式通过递归或迭代来求解f ( 1 ,c)。从f (n, * )开始迭式, f (n, * )由(1 5 - 1)式得出,然后由( 1 5 - 2)式递归计算f (i,*) ( i=n- 1,n- 2,., 2 ),最后由( 1 5 - 2)式得出f ( 1 ,c)。
对于例1 5 - 2,若0≤y<1 0,则f ( 3 ,y) = 0;若y≥1 0,f ( 3 ,y) = 1 5。利用递归式(1 5 - 2),可得f (2, y) = 0 ( 0≤y<10 );f(2,y)= 1 5(1 0≤y<1 4);f(2,y)= 1 8(1 4≤y<2 4)和f(2,y)= 3 3(y≥2 4)。因此最优解f ( 1 , 11 6 ) = m a x {f(2,11 6),f(2,11 6 - w1)+ p1} = m a x {f(2,11 6),f(2,1 6)+ 2 0 } = m a x { 3 3,3 8 } = 3 8。
现在计算xi 值,步骤如下:若f ( 1 ,c) =f ( 2 ,c),则x1 = 0,否则x1 = 1。接下来需从剩余容量c-w1中寻求最优解,用f (2, c-w1) 表示最优解。依此类推,可得到所有的xi (i= 1.n) 值。
在该例中,可得出f ( 2 , 11 6 ) = 3 3≠f ( 1 , 11 6 ),所以x1 = 1。接着利用返回值3 8 -p1=18 计算x2 及x3,此时r = 11 6 -w1 = 1 6,又由f ( 2 , 1 6 ) = 1 8,得f ( 3 , 1 6 ) = 1 4≠f ( 2 , 1 6 ),因此x2 = 1,此时r= 1 6 -w2 = 2,所以f (3,2) =0,即得x3 = 0。
动态规划方法采用最优原则( principle of optimality)来建立用于计算最优解的递归式。所谓最优原则即不管前面的策略如何,此后的决策必须是基于当前状态(由上一次决策产生)的最优决策。由于对于有些问题的某些递归式来说并不一定能保证最优原则,因此在求解问题时有必要对它进行验证。若不能保持最优原则,则不可应用动态规划方法。在得到最优解的递归式之后,需要执行回溯(t r a c e b a c k)以构造最优解。
编写一个简单的递归程序来求解动态规划递归方程是一件很诱人的事。然而,正如我们将在下文看到的,如果不努力地去避免重复计算,递归程序的复杂性将非常可观。如果在递归程序设计中解决了重复计算问题时,复杂性将急剧下降。动态规划递归方程也可用迭代方式来求解,这时很自然地避免了重复计算。尽管迭代程序与避免重复计算的递归程序有相同的复杂性,但迭代程序不需要附加的递归栈空间,因此将比避免重复计算的递归程序更快。
3.2 应用
3.2.1 0/1背包问题
1. 递归策略
在例3 - 4中已建立了背包问题的动态规划递归方程,求解递归式( 1 5 - 2)的一个很自然的方法便是使用程序1 5 - 1中的递归算法。该模块假设p、w 和n 为输入,且p 为整型,F(1,c) 返回f ( 1 ,c) 值。
程序15-1 背包问题的递归函数
int F(int i, int y)
{// 返回f ( i , y ) .
if (i == n) return (y < w[n]) ? 0 : p[n];
if (y < w[i]) return F(i+1,y);
return max(F(i+1,y), F(i+1,y-w[i]) + p[i]);
}
程序1 5 - 1的时间复杂性t (n)满足:t ( 1 ) =a;t(n)≤2t(n- 1)+b(n>1),其中a、b 为常数。通过求解可得t (n) =O( 2n)。
贪心,动态,回溯 ////////////////////////////////////////// GREEDY_KNAPSACK(p1,w1,M1,n1) float p1[]; float w1[]; float M1; int n1; { float a[10],s,rc; int x[10],b[10],i,j,k,t; for (i=1;i <=n1;i++) { a[i]=p1[i]/w1[i]; b[i]=i; } for (j=1;j <=n1-1;j++) { for (i=1;i <=n1-j;i++) if (a[i] <a[i+1]) { s=a[i];a[i]=a[i+1];a[i+1]=s; t=b[i];b[i]=b[i+1];b[i+1]=t; }; } rc=M1; for (i=1; i <=n1; i++) { j=b[i]; if (w1[j] <=rc) {x[j]=1; rc=rc-w1[j];} else x[j]=0; } printf( "0-1背包问题贪心算法的最优解x(n)如下:/n "); for (k=1; k <=n1; k++) printf( "%4d ",x[k]); printf( "/n "); } ///////////////////////////////////////////// DKNAP(p2,w2,M2,n2) float p2[],w2[]; float M2; int n2; { int l,h,u,i,j,k,r,next; int F[10],x[10]; float P[1000],W[1000],pp,ww,PX[10],WX[10],PY[10],c; F[0]=1; P[1]=W[1]=0; l=h=1; F[1]=next=2; for (i=1; i <=n2; i++) { k=l; r=l; while (r <=h) { if (W[r]+w2[i] <=M2) r++; else break; } u=r-1; for (j=1; j <=u; j++) { pp=P[j]+p2[i]; ww=W[j]+w2[i]; while (k <=h && W[k] <ww) { P[next]=P[k]; W[next]=W[k]; next++; k++; } if (k <=h && W[k]==ww) { pp=max(pp,P[k]); k++; } if (pp> P[next-1]) { P[next]=pp; W[next]=ww; next++; } while (k <=h && P[k] <=P[next-1]) k++; } while (k <=h) { P[next]=P[k]; W[next]=W[k]; next++; k++; } //对Si+1置初值 PX[i-1]=P[h]; WX[i-1]=W[h]; l=h+1; h=next-1; F[i+1]=next; } c=M2; for (i=n2-1; i> =0;i--) { j=F[i]; while (j <=F[i+1]-1) { if (W[j]+w2[i+1] <=c) j++; else break; } u=j-1; if (u <F[i]) PY[i]=0; else PY[i]=P[u]+p2[i+1]; if (PX[i]> PY[i]) x[i+1]=0; else {x[i+1]=1; c=c-w2[i+1];} //printf( "%d ",x[i+1]); //printf( "/n "); } printf( "0-1背包问题动态规划方法的最优解x(n)如下:/n "); for (i=1;i <=n2;i++) printf( "%4d ",x[i]); printf( "/n "); } ///////////////////////////////////////////// BKNAP(p3,w3,M3,n3) float p3[],w3[],M3; int n3; { int k,i,j,y[10],x[10],b[10]; float cw,cp,fp,fw,a[10],s,t; for (i=1;i <=n3;i++) { a[i]=p3[i]/w3[i]; b[i]=i; } for (j=1;j <=n3-1;j++) { for (i=1;i <=n3-j;i++) if (a[i] <a[i+1]) { s=a[i];a[i]=a[i+1];a[i+1]=s; t=b[i];b[i]=b[i+1];b[i+1]=t; }; } for (i=1;i <=n3;i++) printf( "%4d/n ",b[i]); printf( "/n "); cw=cp=0; k=1; fp=-1; for (k=1;;k++) { while (k <=n3 && cw+w3[k] <=M3) { cw+=w3[k]; cp+=p3[k]; y[k]=1; k++; } if (k> n3) { fp=cp; fw=cw; k=n3; for (i=1;i <=k;i++) x[b[i]]=y[i]; break; } else y[k]=0; while (bound(cp,cw,k,M3,n3,w3,p3) <=fp) { while (k!=0 && y[k]!=1) k-=1; if (k==0) return; y[k]=0; cw-=w3[k]; cp-=p3[k]; } } printf( "0-1背包问题回溯方法的最优解x(n)如下:/n "); for (i=1; i <=n3; i++) { //j=b[i]; printf( "%4d ",x[i]); } printf( "/n "); printf( "%4f,%4f ",fp,fw); printf( "/n "); } float bound(zp,zw,k,M4,n4,ww,pp) float zp,zw,M4,ww[],pp[]; int k,n4; { int i; float b,c; b=zp;c=zw; for (i=k+1;i <=n4;i++) { c+=ww[i]; if (c <=M4) b+=pp[i]; else return (b+(1-(c-M4)/ww[i])*pp[i]); } return (b); }