引言
动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题。但是在实际应用中,很多问题经分解得到的子问题往往不是互相独立的。
而且不同子问题的数目常常只有多项式量级。在用分治法求解时,有些子问题被重复计算了许多次。如果能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,就可以避免大量重复计算,从而得到多项式时间算法。
动态规划法
- 动态规划算法的基本要素:
- 最优子结构:最优解包含着其子问题的最优解
- 子问题的重叠性质:递归算法求解问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。
最优子结构性质是问题能用动态规划算法求解的前提
- 动态规划算法主要设计步骤:
- 找出最优解的性质,并刻画其结构特征;
- 递归地定义最优值;
- 以自底向上的方式计算出最优值;
- 根据计算最优值时得到的信息,构造最优解。
特殊形式:备忘录算法
备忘录方法是动态规划算法的变形,它通过分治思想对原问题进行分解,以存储子问题的解的方式解决冗余计算,并采用自顶向下的递归方式获取问题的最终解。与动态规划算法的不同之处是动态规划算法的递归方式是自底向上递归求解,而备忘录方法的递归方式是自顶向下递归求解。当一个问题的所有子问题都至少要解一次时,使用动态规划算法。当子问题空间中的部分子问题不需要求解时,使用备忘录方法。
分治与动态规划比较:
二者都要求原问题具有最优子结构性质,都是将原问题分而治之,分解成若干个规模较小的子问题。然后将子问题的解合并,形成原问题的解。
区别:
- 分治法将分解后的子问题是相互独立的;动态规划将分解后的子问题间有相互联系,有重叠部分。
实现方法: - 分治法通常利用递归求解;动态规划通常利用迭代法自底向上求解,但也能用具有记忆功能的递归法自顶向下求解(备忘录算法)。
例题
一、动态规划——矩阵连乘问题
给定n个矩阵{A1,A2,…,An},其中,Ai与Ai+1是可乘的,(i=1,2 ,…,n-1)。用加括号的方法表示矩阵连乘的次序,不同的计算次序计算量(乘法次数)是不同的,找出一种加括号的方法,使得矩阵连乘的次数最小。
抽象描述:
完全加括号的矩阵连乘积可递归地定义为:
1)单个矩阵是完全加括号的;
2)矩阵连乘之积 A 是完全加括号的则A可表示为2个完全加括号的矩阵连乘积B和C 的乘积并加括号,即A=(BC)
3)输入:向量 P = < P0, P1, … , Pn >, 其中 P0, P1, …, Pn为 n 个矩阵的行数与列数
4)输出:矩阵连乘法加括号的位置.(最优解) 矩阵连乘法加括号后的乘法次数(最优值)
举个例子,比如矩阵A1,A2,A3…,An,那么它对应向量 P = < P0, P1, … , Pn >,
A1的行数为P0,列数为P1,…… ,An的行数为Pn-1,列数为Pn
矩阵A: i 行 j 列,B: j 行 k 列,以元素相乘作基本运算,计算 AB的工作量
AB: i 行 k列,计算每个元素需要做 j 次乘法, 总计乘法次数 为 i j k
算法:
!!!k的位置只有 j-i 种可能。多次划分k取最小值。
特征:计算A[i:j]的最优次序所包含的计算矩阵子链 A[i:k]和A[k+1:j]的次序也是最优的。
假设:
m[i,j]:计算A[i:j]所需要的最少数乘次数,
那么,m[1,n]:原问题的最优值。(A1…Ak)(Ak+1…An)是最优解。
最优解(A1…Ak)(Ak+1…An)的代价方程
m[i][i]=0 计算A[i:i]的最小乘法数 Ai的维数是Pi-1×Pi
m[i][j]=m[i][k]+m[k+1][j]+Pi-1PkPj
!!将m[i][j]的断开位置k存入s[i][j]中,方便以后根据s[i][j]可求出最优解。
最优值的代价方程
void MatrixChain(int *p,int n,int **m,int **s)
{
//求最优值
for (int i = 1; i <= n; i++) m[i][i] = 0; //初始化,只有1个矩阵连乘时为0
for (int r = 2; r <= n; r++) //r表示求解m[i][j]中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;//从第一个位置开始划分k,循环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;
}
}
}
}
void TraceBack(int i, int j, int **S)
{
//输出最优解
if (i==j) break;
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;
}
算法MatrixChain的主要计算量取决于算法中对r,i和k的三重循环。循环体内的计算量为O(1),而3重循环的总次数为O(n3)。因此算法的计算时间上界为O(n3)。
矩阵连乘问题递归实现的实现:
int RecurMatrixChain(int i,int j)
{
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[k]*p[j];
if(t<u){
u=t;
s[i][j]=k;
}
return u;
}
}
用算法RecurMatrixChain(1,4)计算m[1:4]的递归树,从该图可以看出,许多子问题被重复计算。
时间复杂度为O(2n)
对求解矩阵连乘问题用递归求解的改进方案-备忘录方法
int MemorizeMatrixChain(int n, int **m, int **s)
{
for(int i=1; i<=n; i++)
for(int j=i; j<=n; j++) m[i][j]=0; //不同点
return LookupChain(1,n);
}
int LookupChain(int i,int j)
{
if (m[i][j] > 0) return m[i][j]; //不同点
if (i == j) return 0;
int u = LookupChain(i,i)+LookupChain(i+1,j)+p[i-1]*p[i]*p[j];
s[i][j] = i;
for (int k = i+1; k < j; k++)
{
int t = LookupChain(i,k)+LookupChain(k+1,j)+p[i-1]*p[k]*p[j];
if (t < u)
{
u = t;
s[i][j] = k;
}
}
m[i][j] = u; //不同点
return u;
}
一共有O(n2)个备忘录记录项m[i][j], i=1,…,n; j=i,…,n。这些记录项的初始化耗费O(n2)
每个记录项只填入一次,每次填入时,不包括填入其他记录项的时间,耗费O(n)时间。因此LookupChain填入O(n2)个记录项总共耗费O(n3)计算时间。
由此可见,通过使用备忘录方法,直接递归算法的计算时间从O(2n)降至O(n3)
二、动态规划——最大字段和问题
给定n个数(有正有负)的序列 (a1, a2, … , an),求连续的ai到aj的最大的和,如果求出来是负数,则返回0,否则,返回该值。
1.蛮力法:求所有的字段的和,取其中的最大值。
时间复杂度O(n2)
void MaxSum( int n, int *a, int &besti, int &bestj)
{
int temp,sum=0;
for(int i=1;i<=n;i++){
temp=0;
for(int j=i;j<=n;j++)
{
temp+=a[j];
if(temp>sum){
sum=temp;
besti=i; bestj=j;
}
}
}
}
2.动态规划法
那么就有数组C[ ]={C1,C2,C3……Cn} 取最大值即得到最大字段和。
那么C[1]的就确定了,如何求数组C中后面元素的值呢?先得到递推方程:
在编程中转化为:如果C[i-1]<=0的话,那么C[i]一定等于A[i]
否则C[i]等于A[i]+C[i-1]
int MaxSum(int n, int *a, int *c, int *d)
{
//动态规划实现-自底向上求最优值
int sum=0;
int *b=new int[100];
b[0]=0; //数组b相当于上面的数组C
for(i=1; i<=n; i++){
if(b[i-1]>0){
b[i]=b[i-1]+a[i];
c[i]=1; //数组c做标记,为1的元素构成最大字段和
}
else{
b[i]=a[i]; c[i]=0;
}
if(b[i]>sum){
sum=b[i]; (*d)=i; //最大字段和的结束下标
}
}
return sum;
}
void output(int *c, int d)
{
//输出最优解
int i=d;
do{
if(i==d) printf("=%d",a[i]);
else printf("+%d",a[i]);
}
while(c[i--]==1);
}
时间复杂度T(n)=O(n)
三、动态规划——最长公共子序列问题
子序列的定义:
若给定序列X={x1,x2,…,xm},则另一序列Z={z1,z2,…,zk}是X的子序列是指存在一个严格递增下标序列{i1,i2,…,ik},使得对于所有j=1,2,…,k,有:zj=xi成立。
例如,序列Z={B,C,D,B}是序列X={A,B,C,B,D,A,B}的子序列,则相应的递增下标序列为{2,3,5,7}。
给定2个序列X和Y,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列。
给定2个序列X={x1,x2,…,xm}和Y={y1,y2,…,yn},找出X和Y的最长公共子序列。
-
问题的分析:子问题间的依赖关系
设序列X={x1,x2,…,xm}和Y={y1,y2,…,yn}的最长公共子序列为Z={z1,z2,…,zk} ,则
(1)若xm=yn,则zk=xm=yn,且zk-1是xm-1和yn-1的最长公共子序列。
(2)若xm≠yn且zk≠xm,则Z是xm-1和Y的最长公共子序列。
(3)若xm≠yn且zk≠yn,则Z是X和yn-1的最长公共子序列。
满足优化原则和子问题重叠性。由此可见,2个序列的最长公共子序列包含了这2个序列的前缀的最长公共子序列。因此,最长公共子序列问题具有最优子结构性质。 -
算法设计:
由最长公共子序列问题的最优子结构性质建立子问题最优值的递归关系。
用一个二维数组c[i][j]记录序列和的最长公共子序列的长度。其中,Xi={x1,x2,…,xi},Yj={y1,y2,…,yj}。当i=0或j=0时,空序列是Xi和Yj的最长公共子序列,故此时C[i][j]=0。其它情况下,由最优子结构性质可建立递归关系如下:
从数组C中查找一个最长的公共子序列
i和j分别从m,n开始,递减循环直到i = 0,j = 0。其中,m和n分别为两个串的长度。
如果x[i] == y[j],则将str[i]字符插入到子序列内,i- -,j- -;
如果x[i] != y[j],则比较C[i,j-1]与C[i-1,j],C[i,j-1]大,则j- -,否则i- -;(如果相等,则任选一个)
注:构建c[i][j]表需要O(m*n),输出1个LCS的序列需要O(m+n)。
为了方便比较,直接设置一下寻找方向,记作标记函数:
1代表相等,斜向上; 2代表←;3代表↑。
!!二维数组下标从1开始记录,且初始化时第一行第一列均为0。c[m][n]即为最优值:最大长度。
void LCSLength(int m,int n,char *x,char *y,int **c,int **b)
{
int i,j;
for(i = 1; i <= m; i++) c[i][0] = 0;
for(i = 1; i <= n; i++) c[0][i] = 0;
for(i = 1; i <= m; i++)
for(j = 1; j <= n; j++){
if(x[i]==y[j]){
c[i][j]=c[i-1][j-1]+1;
b[i][j]=1;
}
else if(c[i-1][j]>=c[i][j-1]){
c[i][j]=c[i-1][j];
b[i][j]=2;
}
else{
c[i][j]=c[i][j-1];
b[i][j]=3;
}
}
}
void LCS(int i,int j,char *x,int **b)
{
if(i==0 || j==0) return;
if(b[i][j]== 1){
LCS(i-1,j-1,x,b);
cout << x[i];
}
else if(b[i][j]== 2) LCS(i-1,j,x,b);
else LCS(i,j-1,x,b);
}
四、动态规划——0-1背包问题
问题描述:
给定n种物品和一背包。物品i的重量是wi,其价值为vi,背包的容量为C。问:应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
目标函数
约束条件
- 算法描述:
设所给0-1背包问题的子问题的最优值为m(i,j),即m(i,j)是背包容量为j,可选择物品为i,i+1,…,n时0-1背包问题的最优值。由0-1背包问题的最优子结构性质,可以建立计算m(i,j)的递归式如下:
所以,特别地当i等于n时,只能装入最后一件物品,则有:
所以,二维数组中m[1][C]即为最优值(右上角),x向量<x1,x2,……xn>即为最优解。
void Knapsack(int *v,int *w, int c,int n,int (*m)[10])
{
int jMax,i,j;
jMax=min(w[n]-1,c);
for(j=0;j<=jMax;j++) m[n][j]=0;
for(j=w[n];j<=c;j++) m[n][j]=v[n]; //这部分是计算m[n][j]的值。
for(i=n-1;i>=1;i--){
jMax=min(w[i]-1,c);
for(j=0;j<=jMax;j++) m[i][j]=m[i+1][j];
for(j=w[i];j<=c;j++){
int a=m[i+1][j-w[i]]+v[i];
m[i][j]=max(a,m[i+1][j]);
}
}
}
void Traceback(int (*m)[10],int *w,int *x,int c,int n)
{
for(int i=1;i<n;i++)
if(m[i][c]==m[i+1][c]) x[i]=0;
else{
x[i]=1; c-=w[i];
}
if(m[n][c]) x[n]=1;
else x[n]=0;
}
//数组x中存放最优解,数组m中m[1][c]存放最优值。