前言
动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
不同:
适用于动态规划法求解的问题,经分解得到的子问题往往不是互相独立的。
若用分治法来求解这类问题,则分解得到的子问题数目太多,需要耗费指数级的时间。
基本思想:
因为,分解成子问题求解时,有些子问题被重复计算许多次。如果能够保存已经解决的子问题的答案,而在需要时再找出已求得的答案,就可以避免大量的重复计算,从而得到多项式级的计算时间。
为了达到此目的,可用一个表来记录所有已解决的子问题的答案。
不管该子问题以后是否被用到,只要被计算过,就将其结果填入表中。
动态规划算法步骤:
动态规划算法适用于解最优化问题。
(1)找出最优解的性质,并刻画其结构特征。
(2)递归地定义最优值。
(3)以自底向上的方式计算出最优值。
(4)根据计算最优值时得到的信息,构造最优解。
其中,(1)~(3)是基本步骤。
一、矩阵连乘问题
可乘条件
若两个矩阵𝑨_(𝒎×𝒏)×𝑩_(𝒑×𝒒)相乘
需要满足𝒏=𝒑
2、改写为
𝑨_(𝒑×𝒒)×𝑩_(𝒒×𝒓)
乘法次数=𝒑×𝒒×𝒓
𝑨_(𝒑×𝒒)×𝑩_(𝒒×𝒓)=𝑪_(𝒑×𝒓)
3、相乘程序
见3_1.cpp
4、连乘次序影响乘法次数
程序3_1
//P45矩阵连乘问题
#include <iostream>
using namespace std;
void matrixMultiply(int **a, int **b, int **c,int ra,int ca,int rb,int cb)//用二级指针,二维数组动态生成,界限由调用传入(参考P304-305)
{
if(ca!=rb)
error("矩阵不可乘");
for(int i=0;i<ra;i++)//各行
for(int j=0;j<cb;j++)//各列
{
int sum=a[i][0]*b[0][j];//初值
for(int k=1;k<ca;k++)
sum+=a[i][k]*b[k][j];//乘实现累计
c[i][j]=sum;//乘积记入c
}
}
int main()
{
int **a,**b,**c,ra,ca,rb,cb,rc,cc;
ra=2;ca=3;rb=3;cb=2;rc=2;cc=2;
a=new int *[ra];
for(int i=0;i<ra;i++)
a[i]=new int[ca];//动态产生二维数组a,即矩阵a
//用同样的方式生成二维数组b和c
for(int j=0;j<ra;j++)
for(int k=0;k<ca;k++)
a[j][k]=somevalue;//用这种方式为数组a和b赋值
martixMultiply(a,b,c,ra,ca,rb,cb);
//释放数组a,b,c的空间
return 0;
}
用动态规划法解矩阵连乘的最优计算次序问题
1、分析最优解的结构
记𝑨[𝒊:𝒋]是𝑨_𝒊 𝑨_(𝒊+𝟏)⋯𝑨_𝒋
问题:𝑨[𝟏:𝒏],可以在𝒌=𝟏,𝟐,⋯𝒏−𝟏处断开,若𝑨[𝟏:𝒏]最优,则𝑨[𝟏:𝒌]与𝑨[𝒌+𝟏:𝒏]也最优
3、计算最优值
见3_2.cpp
例:以𝒏=𝟔为例,说明程序计算过程
𝑨𝟏 𝑨𝟐 𝑨𝟑
30×35 35×15 15×5
𝑨𝟒 𝑨𝟓 𝑨𝟔
5×10 10×20 20×25
程序3_2
//P47计算最优值
void MatrixChain(int *p, int n, int **m, int **s)//P一维数组记下标;n矩阵个数;m表;s记分段
{
for(int i=1;i<=n;i++)
m[i][i]=0;//对角线上都填0
for(int r=2;r<=n;r++)//在m表中,从第2条填到第n条(对角线)
for(int i=1;i<=n-r+1;i++)//i是行下标,从第1行开始填一个倒三角的表
{
int j=i+r-1;//列下标
m[i][j]=0+m[i+1][j]+p[i-1]*p[i]*p[j];//实现递归计算,先初始化第1项,省略了m[i][i]
s[i][j]=i;//s记分段点,此为s的初值
for(int k=i+1;k<j;k++)//递归式从k=i+1到j-1,找最小
{
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;//记入划分位置
}
}
}
}
3、计算最优值
n=6 A1 A2 A3 A4 A5 A6
30×35 35×15 15×5 5×10 10×20 20×25
4.构造最优解
算法MatrixChain只是计算出了最优值,并未给出最优解。
也就是说,通过MatrixChain的计算,只知道最少乘法次数,还不知道具体应该按什么次序来做矩阵乘法才能达到最少的
乘法次数,即𝑨[𝟏:𝒏]的完全加括号方式。
事实上,MatrixChain已记录了构造最优解所需要的全部信息。𝒔[𝒊][𝒋]中的数表明,最佳方式应在矩阵的何处断开。
见3_3.cpp
程序3_3
//构造最优解 P48-49
void Traceback(int i,int j,int **s)//求A[i:j]的最优解构造,利用s[][],设已知s[i][j]=k
{
if(i==j)
return ;//A[i:i]不必构造,递归出口
Traceback(i,s[i][j],s);//求A[i:k]的构造,记1
Traceback(s[i][j]+1,j,s);//求A[k+1:j]的构造,记2
cout << "Multiply A[" << i << ":" << s[i][j]
<< "]and A[" << (s[i][j] + 1) << ":" << j << "]" << endl;//输出Ai,k And Ak+1,j,表明在k处断开,记3
}
二、动态规划算法的基本要素
从计算矩阵连乘最优次序的动态规划算法可以看出,该算法的有效性依赖于问题本身所具有的两个重要性质:
(1)最优子结构性质
(2)子问题重叠性质
从一般意义上讲,问题所具有的这两个性质是该问题可用动态规划算法求解的基本要素。这对于在设计求解具体问题的算法时,是否选择动态规划算法具有指导意义。
1、最优子结构
设计动态规划算法的第一步通常是要刻画最优解的结构。
当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。
例,矩阵连乘问题中,为了构造𝑨[𝟏:𝒏]的最优解,可寻找断开处𝒌,使得𝑨[𝟏:𝒌]和𝑨[𝒌+𝟏:𝒏]也最优。
问题的最优子结构性质提供了该问题可用动态规划算法求解的重要线索。
2、重叠子问题
在用递归算法自顶向下解此问题时,每次产生的子问题并不总是新问题,有些子问题被反复计算多次。
动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只解一次,而后将其解保存在一个表格中,当再次需要解此子问题时,只是简单地用常数时间查看一下结果。通常,不同的子问题个数随问题的大小呈多项式增长。因此,用动态规划算法通常只需要多项式时间,从而获得较高的解题效率。
为了说明这一点,利用递归式计算𝑨[𝒊:𝒋]的最优值。
见3_4.cpp
考察RecurMatrixChain()的递归情况
程序3_4
//求最优值(P47)的递归算法(P49-50)
int RecurMatrixChain(int i,int j,int *p,int **s)//求A[i:j]最优值,用到p(矩阵行列数),s(分段点),以下函数名简记R
{
if(i==j)
return 0;//递归出口,对应一个矩阵不用乘,次数是0
int u=R(i,i,p,s)+R(i+1,j,p,s)+p[i-1]*p[i]*p[j];//递归第1步,求最小值先取第1项
s[i][j]=i;//s初值
for(int k=i+1;k<j;k++)//递归步k次,比得最小值,k记分段点
{
int t=R(i,k,p,s)+R(k+1,j,p,s)+p[i-1]*p[k]*p[j];
if(t<u)
{
u=t;
s[i][j]=k;
}
}
return u;//返回A[i:j]的最优值
}
3、备忘录方法
备忘录方法是动态规划算法的变形。
与动态规划算法一样,备忘录方法用表格保存已经解决的子问题的答案,在下次需要解此子问题时,只要简单地查看该子问题的解答,而不必重新计算。
不同的是,备忘录方法的递归方式是自顶向下的,而动态规划算法则是自底向上递归的。
因此,备忘录方法的控制结构与直接递归方法的控制结构相同,区别在于备忘录方法为每个解过的子问题建立了备忘录以备需要时查看,避免了相同子问题的重复求解。
见3_5.cpp
程序3_5
//备忘录方法
int RecurMatrixChain(int i,int j,int *p,int **s,int **m)//求A[i:j]最优值,用到p(矩阵行列数),s(分段点),以下函数名简记R,加参数m,m初始值为0,过程略
{
if(m[i][j]>0)
return m[i][j];//若m[i][j]>0表示已经计算过,直接返回
if(i==j)
return 0;
int u=R(i,i,p,s,m)+R(i+1,j,p,s,m)+p[i-1]*p[i]*p[j];//加参数m
s[i][j]=i;
for(int k=i+1;k<j;k++)
{
int t=R(i,k,p,s,m)+R(k+1,j,p,s,m)+p[i-1]*p[k]*p[j];//加参数m
if(t<u)
{
u=t;
s[i][j]=k;
}
}
return m[i][j]=u;//返回A[i:j]的最优值
}
备忘录方法解决大整数乘法也可,但因大整数乘法中子问题无重叠子问题,因此无意义。
0-1背包问题
//物品不可分:动态规划 第3步
#include <iostream>
using namespace std;
template <class Type>
void Knapsack(Type *v, int *w, int n, Type **m)
{
int jMax=min(w[n]-1,c);//找临界值w[n]-1是物品装不进背包的最小重量,并避免物品n比背包容量还要大的情况
//最小子问题,填写最后一行m[n][j]
for(int j=0;j<=jMax;j++)
m[n][j]=0;
for(int j=w[n];j<=c;j++)
m[n][j]=v[n];
for(int i=n-1;i>1;i--)
{
jMax=min(w[i]-1,c);
for(int j=0;j<=jMax;j++)
m[i][j]=m[i+1][j];//物品i装不进去
for(int j=w[i];j<=c;j++)
m[i][j]=max(m[i+1][j],m[i+1][j-w[i]]+v[i]);//能装但不装,装进去,两个值中取较大值
}
//填最后一行
m[1][c]=m[2][c];//若物品1装不进去
if(c>=w[1])//若物品1可以装
m[1][c]=max(m[2][c],m[2][c-w[1]]+v[1]);
}