参考书籍:算法设计与分析——C++语言描述(第二版)
算法设计策略-动态规划法
矩阵连乘
问题描述
给定n个矩阵
设有矩阵A和B,A是
矩阵A和B相乘的数乘(两元素相乘)次数是:
矩阵连乘的计算次序可以用加括号的方式来确定,一旦一个矩阵连乘的计算次序完全确定,也称该连乘积已完全加括号(fully parenthesized)。
完全加括号的矩阵连乘积可递归地定义为:
- 单个矩阵是完全加括号的;
- 矩阵连乘积A是完全加括号的,则A可表示为两个完全加括号的矩阵连乘积B和C的乘积并加括号,即
A=(BC) 。
设
此递推式的解是卡特朗(Catalan)数列:
动态规划法求解
最优子结构
矩阵连乘
矩阵连乘
最优解的递推关系
定义一个二维数组m,用来保存矩阵连乘时所需的最少计算量。
设m是二维数组,
当
当
这里
在上式中,
重叠子问题
由于不同的有序对
可以证明,直接采用递推计算式,具有指数事件,这与检查每一种加括号方式相同。在递归计算时,许多子问题被重复计算多次。这也是采用动态规划法解决此问题的原因之一,用动态规划法求解此问题,可以采用自底向上的方式进行计算。在计算过程中,保存已求得的子问题的解。每个子问题只计算一次,以后可以直接使用,从而避免重复计算。
矩阵连乘算法
//矩阵连乘算法
class MatrixChain
{
public:
MatrixChain(int mSize,int *q);//创建二维数组m和s,一维数组p,并初始化
int MChain();//一般动态规划算法求最优解值,计算最优解值m[0][n-1]
int LookipChain();//备忘录方法计算最优解值
void Traceback();//构造最优解的共有函数,从s构造最优解,机构造矩阵连乘序列的完全加括号形式
...
private:
void Traceback(int i, int j);//构造最优解的私有递归函数
int LookupChain(int i,int j);//备忘录方法私有递归
int *p,**m,**s,n;
};
int MatrixChain::MChain()
{
//求A[0:n-1]的最优解值
for(int i=0;i<n;i++)
m[i][i]=0;
for(int r=2;r<=n;r++)
for(int i=0;i<=n-r;i++){
int j=i+r-1;
m[i][j]=m[i+1][j]+p[i]*p[i+1]*p[j+1];//m[i][j]的初值
s[i][j]=i;
for(int k=i+1;k<j;k++){
int t=m[i][k]+m[k+1][j]+p[i]*p[k+1]*p[j+1];
if(t<m[i][j]){
m[i][j]=t;
s[i][j]=k;
}
}
}
return m[0][n-1];
}
void MatrixChain::Traceback(int i,int j)
{
if(i==j){
cout << 'A' << i;
return ;
}
if(i<s[i][j]) cout << '(';
Traceback(i,s[i][j]);
if(i<s[i][j]) cout << ')';
if(s[i][j]+1<j) cout << '(';
Traceback(s[i][j]+1,j);
if(s[i][j]+1<j) cout << ')';
}
void MatrixChain::Traceback()
{
cout << '(';
Traceback(0,n-1);
cout << ')';
cout << endl;
}
函数MChain
包含三重循环,循环体内的计算量为
备忘录方法
备忘录方法是动态规划法的一个变种。它采用分治法思想,以自顶向下直接递归的方式求最优解,但与分治法不同的是,备忘录方法为每一个已经计算的子问题建立备忘录,即保存子问题的计算结果以备需要时使用,从而避免了相同子问题的求解。
//矩阵连乘的备忘录方法
int MatrixChain::LookupChain(int i,int j)
{
if(m[i][j]>0)
return m[i][j];//子问题已经求解,直接引用
if(i==j)
return 0;//单一矩阵无需计算
int u=LookupChain(i+1,j)+p[i]*p[k+1]*p[j+1];//求最小值
s[i][j]=i;
for(int k=i+1;k<j;k++){
int t=LookupChain(i,k)+LookupChain(k+1,j)+p[i]*p[k+1]*p[j+1];
if(t<u){
u=t;
s[i][j]=k;
}
}
//保存并返回子最优解值
m[i][j]=u;
return u;
}
int MatrixChain::LookupChain()
{
return LookupChain(0,n-1);//返回A[0:n-1]的最优解值
}
备忘录方法的时间复杂度也是
最长公共子序列
问题描述
定义:若给定序列
定义:给定两个序列X和Y,当另一个序列Z既是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列(common subsequence)。
最长公共子序列的穷举法:对于长度为m的序列X,其每个子序列都对应于下标集
动态规划法求解
最优子结构
定理:设
+ 若
+ 若
+ 若
以上定理表明两个序列的最长公共子序列包含了这两个序列的前缀的最长公共子序列,这意味着最长公共子序列具有最优子结构特性。
最优解的递推关系
设有序列
+ 若
+ 若
与矩阵连乘类似,需要使用一个二维数组来保存最长公共子序列的长度,设
最长公共子序列算法
如果直接根据上面的递推式写一个计算
//动态规划法求LCS长度
class LCS
{
public:
//创建二维数组c,s和一维数组a,b,并进行初始化
LCS(int nx,int ny, char *x,char *y);
//求最优解值(最长公共子序列长度)
void LCSLength();
//构造最优解(最长公共子序列)
void CLCS();
...
private:
void CLCS(int i,int j);
int **c,**s,m,n;
char *s,*b;
};
int LCS::LCSLength()
{
for(int i = 1;i<=m;i++)
c[i][0]=0;
for(int i=1;i<n;i++)
c[0][i]=0;
for(int i = 1;i<=m;i++){
for(int j=1;j<n;j++){
if(x[i]==y[j]){
c[i][j]=c[i-1][j-1]+1;
s[i][j]=1;
//由c[i-1][j-1]计算c[i][j]
} else if(c[i-1][j]>=c[i][j-1]){
c[i][j]=c[i-1][j];
s[i][j]=2;
//由从c[i-1][j]得到c[i][j]
} else{
c[i][j]=c[i][j-1];
s[i][j]=3;
//由c[i][j-1]得到c[i][j]
}
}
}
return c[m][n];//返回最优解值
}
设由序列
//构造最长公共子序列
void LCS::CLCS(int i,int j)
{
if(i==0||j==0)
return ;
if(s[i][j]==1){
CLCS(i-1,j-1);
cout<<a[i];
} else if(s[i][j]==2){
CLCS(i-1,j);
} else {
CLCS(i,j-1);
}
}
算法的改进
求LCS长度的程序中数组s是可以省去的,因为
另外,在只求最长公共子序列的长度、无需构造最优解时,也可以只用两行元素空间,时间是