本系列所有代码https://github.com/YIWANFENG/Algorithm-github
动态规划
其实动态规划和递归分治有异曲同工之妙。因为分治中将问题分为若干子问题,但是大部分情况下子问题互相独立,而如果动态规划中大多数子问题不独立(即重叠子问题性质),我们同分治一样,采取从上到下再到上分析,从下到上求解的方法,保留每一个子问题的解(动态规划问题要符合最优子结构),由子解来得到上一层的解。所以求解要先找出最优解的性质,递归定义最优值,自底而上,由计算最优值时得到的信息构造最优解。
(动态规划有“牺牲空间换取时间”的意思,分治很有可能进行大量重复计算,动态规划要把已解决的问题子答案全部保存,以备后续使用)
有些问题很难想到解决方法,不要怕,很多人直接想不出来,这时候去搜一些解答或许对我们有所启发。:-D
矩阵连乘
给定{A1, A2, A3,..., An},其中A(i)与A(i+1)是可乘的,i=1,2,....n-1。求这n个矩阵的连乘积A1A2A3...An以最少的计算量完成计算时的最优计算次序。矩阵乘法是满足结合律。
例如:
A1 = 10*100
A2 = 100*5
A3 = 5*50
A[p0,p1]*A2[p1,p2]=A3[p0,p2]=>,数乘次数=p0*p1*p2。
如果((A1A2)A3)=10*100*5+10*5*50=7500
(A1(A2A3))=100*5*50+10*100*50=75000
由此可见不同计算次序所需数乘是大有不同的。
矩阵连乘可以用加括号来限定计算次序。
规定:
1.单个矩阵是加括号的。
2.矩阵连乘积A 是加括号的,那么A可以表示为两个加括号的矩阵B与C的乘积并加括号,即A=(BC)
用分治法穷举所有计算序列有大量重复计算,并且复杂度随n指数增长,过于低效。
求解:(抄书)
1.第一,刻画该问题最优解的结构特征。
将A(i)A(i+1)...Aj记为A[i:j],计算A[i:j]的最优计算次序,设这个计算次序在矩阵A(k)与A(k+1)之间将他断开,1<=k<=n,则其对应的完全加括号方式为((A1...Ak)(A(k+1)...An)).依次矩阵相乘先计算A[1:k]与A[k+1:n],再将计算结果相乘得到A[1:n].那么总计算量为A[1:k]与A[k+1,n]的计算量加上A[1:k]与A[k+1:n]相乘的计算量。由最优子结构可知A[1:k]与A[k+1,n]的计算次序也是最优的。
2.递归关系建立。
设计算A[i:j],1<=i<=j<=n,所需要的最少数乘次数为m[i][j],则原题最优解为m[1][n].
当i=j,A[i:j]=Ai(单个基本矩阵),m[i][i]=0 , i=1,2,...n;
当i<j,可借助最优子结构性质来计算m[i][j](这也就是第一步的目的)。
若A[i:j]在A(k)与A(k+1),i<k<j之间断开,那么m[i][j]=m[i][k]+m[k+1][j]+p(i-1)p(k)p(j)。
(P(i)为矩阵行或者列数)。 K in{i,i+1,i+2,...j_1}。所以k为这j-i个位置中计算量最小的那个。
所以m[i][j]可递归定义为
M[i][j] = 0 ,i == j
M[i][j] = min{m[i][k]+m[k+1][j]+p(i-1)p(k)p(j)} ,i<=k<=j, i<j
计算时可以保存断开位置k到另一个矩阵。
3.计算最优值。
自底而上。
可见动态规划可节省大量计算。
主要借助第二步推导
动态规划:
void MatrixChain(int *p,int n) {
//p[i]为矩阵序列中矩阵[i]的行数==下一矩阵的列数
//n==矩阵数,m记录计算数量,s记录断开位置
for(int i=1; i<=n; ++i) m[i][i]=0;//单个矩阵计算次数为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) {
//求解本次分组(含有j-i个元素的分组)中最小的计算次数
intt=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;
}
}
}
}
}
递归式Code,
int RecurMatrixChain(inti,int j) {
//递归式并采取备忘录方法
//需初始化m[i][j]=-1;
if(m[i][j]>0) return m[i][j]; //备忘记录,以此减少递归计算量
if(i==j) return 0;
int u = RecurMatrixChain(i,i)+RecurMatrixChain(i+1,j)+p[i-1]*p[k]*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;
}
}
m[i][j]=u;
return u;
}
最长公共子序列
给定两个序列X、Y,求解他们的最长公共子序列。
对于序列X={x1,x2,x3....xn},另一序列Z={z1,z2,z3...}是X子序列的必要条件是每一个zi都属于X,并且zi对应在X中的相同数值的下标是递增的。
设序列X ={x1,x2,x3...xm} 、 Y ={y1,y2,y3....yn} 最长公共子序列Z={z1,z2,z3...zk}.则:
1.若 xm == yn ,有zk == xm ==yn,且Z(k-1)是X(m-1)与Y(n-1)的最长公共子序列(最优子结构)。若xm!=yn &&zk!=xm,有Z是X(m-1)与Y的最长公共子序列。
2.若xm!=yn && zk != yn,有Z是X与Y(n-1)的最长公共子序列。
X(m-1)={x1,x2,x3...x(m-1)} Y(n-1)={y1,y2,y3...y(n-1)} Z(k-1)={z1,z2,z3...z(k-1)}。
子问题递归结构:
要找X与Y的最长公共子序列,可以按照上面的推导,进行递归寻找。
用c[i][j]记录序列Xi、Yj 的最长公共子序列长度,Xi={x1,x2,x3..xi},Y={y1,y2,...yi}
显然当i==0 ||j==0时,空序列是二者最长公共子序列。即C[i][j] == 0。
那么
C[i][j] = 0 ,i==0 || j==0
C[i-1][j-1]+1 ,i,j> 0 && xi != yi
Max{c[i][j-1],c[i-1][j]} ,i,j> 0 && xi != yi
int c[n+1][m+1]; //记录字串长度
int b[n+1][m+1]; //记录c[i][j]来源
void LCSLength(int n,intm,const char *x,const char *y) {
//n序列x字符数,m序列y字符数
for(int i=0;i<=n;++i) c[i][0]=0;
for(int i=0;i<=m;++i) c[0][i]=0;
for(int i=1;i<=n;++i) {
for(intj=1;j<=m;++j) {
if(x[i-1]==y[j-1]) {
//x[i]==y[i],才是真正的在字串中
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,intj,const char *x) {
if(i==0 || j==0) return ;
if(b[i][j]==1) {
LCS(i-1,j-1,x);
cout<<x[i-1];
} else if(b[i][j]==2)
LCS(i-1,j,x);
else
LCS(i,j-1,x);
}
最大字段和
给定n个整数组成的序列a1,a2,...an,求该序列形如的子段和最大。
Max() 1<=i<=j<=n
第一我们可以遍历所有子段和来解决
int MaxSum(int n,inta[],int &start,int &end)
{
//遍历法求最大子段和
//n元素数,a元素数组
//start选中的字段开始元素下标
//end选中的字段结束元素下标
int sum=0;
for(int i=0;i<=n;++i) {
int this_sum = 0;
for(intj=i;j<=n;++j) {
this_sum += a[j];
if(this_sum>sum) {
sum = this_sum;
start= i;
end= j;
}
}
}
return sum;
}
第三动态规划分析
记b[j]为第1-j个元素的最大子段和,则所求最大子段和为b[n],
依据b[j]定义可知b[j-1]>0时b[j]=b[j-1]+a[j],否则b[j]=a[j].
即b[j]的动态规划递归式为
b[j]=max{b[j-1]+a[j],a[j] }, 1<=j<=n;
Code:
int MaxSum(int n,inta[])
{
//动态规划求最大子段和
int sum = 0,b= 0;
for(int i=0;i<n;++i) {
if(b>0)b+=a[i];
else b = a[i];
if(b>sum) sum =b;
}
return sum;
}