[toc]
Introduction
动态规划一般也只能应用于有最优子结构的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决。
dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems.
适合采用动态规划方法的最优化问题的俩个要素:最优子结构性质,和子问题重叠性质。
最优子结构:如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。意思就是,总问题包含很多个子问题,而这些子问题的解也是最优的。
重叠子问题:子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了 这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下 结果,从而获得较高的效率。
总结而言,一个问题是该用递推、贪心、搜索还是动态规划,完全是由这个问题本身阶段间状态的转移方式决定的:
每个阶段只有一个状态->递推;
每个阶段的最优状态都是由上一个阶段的最优状态得到的->贪心;
每个阶段的最优状态是由之前所有阶段的状态的组合得到的->搜索;
每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到而不管之前这个状态是如何得到的->动态规划。
动态规划求解
求解动态规划的关键,是对问题状态的定义和状态转移方程的定义。动态规划中递推式的求解方法不是动态规划的本质。动态规划算法分以下4个必须步骤:
划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。注意这若干个阶段一定要是有序的或者是可排序的(即无后向性),否则问题就无法用动态规划求解。
描述最优解的结构,即状态的定义:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。 无后向性即每个阶段的最优状态可以从之前某个阶段的某个或某些状态直接得到但是不用管之前的状态是如何得到的。
递归定义最优解的值,即状态转移方程的定义。
按自底向上的方式计算最优解的值 。
由计算出的结果构造一个最优解。 //此步如果只要求计算最优解的值时,可省略。
这里我们以LIS问题做一个实例讲解,给定一个数列,长度为N,求这个数列的最长上升(递增)子数列(LIS)的长度。以1 7 2 8 3 4为例,这个数列的最长递增子数列是 1 2 3 4,长度为4;次长的长度为3, 包括 1 7 8; 1 2 3 等。首先我们对这个问题进行阶段划分,可以看出求某个数列的最长递增子数列这个问题肯定可以划分为多个阶段进行处理,即具备了最优子结构与重叠子问题。下面我们定义解的结构,即状态,给定一个数列,长度为N,设$F_k$为:以数列中第k项结尾的最长递增子序列的长度。求解$F_1 \dots F_N$中的最大值。那么状态转移方程,即DP方程也就是:
$$F_1 = 1$$
$$F_k = max(F_i + 1 | A_k > A_i, i \in (1 \dots k-1))(k>1)$$
LIS(最长递增子序列)
#include<iostream>
using namespace std;
/*
**该程序针对上一个程序的修改就是:
有一种特殊情况:求状态d[i]时,发现有2个不同的最长子序列
而上面的方法只是保存了其中的一种。
在此程序中为了在一个数组result[i][]中保存多条最长子序列,
我采用了间隔符的方式,PAUSE表示一条子序列的结束,用来间隔
下一条子序列在数组为设置EBD表示后面没有数据了
*/
#define MAXSIZE 100
#define END -100
#define PAUSE -99
int len[MAXSIZE];
int result[MAXSIZE][MAXSIZE];
void LIS(int* a,int n)
{
int i,j,z,k;
int length=1;
result[0][0]=a[0];
result[0][1]=END;
for(i=0;i<n;i++)
{
len[i]=1;
for(j=0;j<i;j++)
if(a[j]<=a[i]) //当a[j]<a[i]时,符合状态满足的条件,此时分2中情况
{
//第一种情况:len[j]+1大于len[i],此时len[i]对应的状态解集
//应当被覆盖,原来的没用了。
if(len[j]+1>len[i])
{
len[i]=len[j]+1;
for(k=0,z=0;result[j][z]!=END;z++,k++)
{
if(result[j][z]!=PAUSE) //
result[i][k]=result[j][z];
else //PAUSE表示一条记录已结束,下一条开始
{ //此时需要的处理是加入自己a[i],同时也设置标记符PAUSE
result[i][k++]=a[i];
result[i][k]=PAUSE;
}
}
result[i][k++]=a[i];
result[i][k]=END;
}
//第二种情况,len[j]+1等于len[i],此时说明,len[i]以前对应的
//result[i][]是有用的,不应该覆盖掉(保留的原因是因为我们题目要求是显示所有的子序列结果)
else if(len[j]+1==len[i])
{
for(k=0;result[i][k]!=END;k++)
;
result[i][k++]=PAUSE;
for(z=0;result[j][z]!=END;z++,k++)
{
if(result[j][z]!=PAUSE) //
result[i][k]=result[j][z];
else //PAUSE表示一条记录已结束,下一条开始
{
result[i][k++]=a[i];
result[i][k]=PAUSE;
}
}
result[i][k++]=a[i];
result[i][k]=END;
}
}
if(length<len[i])
length=len[i];
}
cout<<"长度为:"<<length<<endl;
for(z=0;z<n;z++)
if(len[z]==length)
{
for(i=0;result[z][i]!=END;i++)
{
if(result[z][i]==PAUSE)
{
i++;
cout<<endl;
}
cout<<result[z][i]<<" ";
}
cout<<endl;
}
}
int main()
{
int a[]={1,3,2,9,11};
LIS(a,5);
return 0;
}
LCS(最长公共子序列)
一个字符串的子序列,是指从该字符串中 去掉任意多个字符 后剩下的字符在 不改变顺序的情况下 组成的新字符串。 最长公共子序列,是指多个字符串可具有的长度最大的公共的子序列。动态规划采用二维数组来标识中间计算结果,避免重复的计算来提高效率。
由最长公共子序列问题的最优子结构性质可知,要找出$X= \lbrace x1, x2, …, xm\rbrace$和$Y=\lbrace y1, y2, …, yn\rbrace$的最长公共子序列,可按以下方式递归地进行:当xm=yn时,找出Xm-1和Yn-1的最长公共子序列,然后在其尾部加上xm(=yn)即可得X和Y的一个最长公共子序列。当xm≠yn时,必须解两个子问题,即找出Xm-1和Y的一个最长公共子序列及X和Yn-1的一个最长公共子序列。这两个公共子序列中较长者即为X和Y的一个最长公共子序列。
由此递归结构容易看到最长公共子序列问题具有子问题重叠性质。例如,在计算X和Y的最长公共子序列时,可能要计算出X和Yn-1及Xm-1和Y的最长公共子序列。而这两个子问题都包含一个公共子问题,即计算Xm-1和Yn-1的最长公共子序列。
与矩阵连乘积最优计算次序问题类似,我们来建立子问题的最优值的递归关系。用c[i,j]记录序列Xi和Yj的最长公共子序列的长度。其中Xi=<x1, x2, …, xi>,Yj=<y1, y2, …, yj>。当i=0或j=0时,空序列是Xi和Yj的最长公共子序列,故c[i,j]=0。其他情况下,由定理可建立递归关系如下:
//求最长的公共子序列
#include<iostream>
#include<string>
using namespace std;
#define MAXSIZE 300
string a;
string b;
int c[MAXSIZE][MAXSIZE];
int d[MAXSIZE][MAXSIZE];
//动态规划的方式求解最长公共子序列问题
void LCS(int m,int n)
{
int i,j;
for(i=1;i<=m;i++)
for(j=1;j<=n;j++)
{
if(a[i-1]==b[j-1])
{
c[i][j]=c[i-1][j-1]+1;
d[i][j]=0;
}
else if(c[i-1][j]>c[i][j-1])
{
c[i][j]=c[i-1][j];
d[i][j]=1;
}
else
{
c[i][j]=c[i][j-1];
d[i][j]=-1;
}
}
}
void display_LCS(int m,int n) //采用回溯方式
{
int i=m,j=n;
int len=c[m][n];
char s[MAXSIZE];
s[len--]='\0';
while(i>0&&j>0)
{
if(d[i][j]==0)
{
s[len]=a[i-1];
len--;
i--;
j--;
}
else if(d[i][j]==1)
i--;
else
j--;
}
cout<<s;
}
int main()
{
a="ABCBDAB";
b="BDCABA";
int m,n;
m=a.size();
n=b.size();
LCS(m,n);
cout<<c[m][n]<<endl;
display_LCS(m,n);
return 0;
}
在序列X={A,B,C,B,D,A,B}和 Y={B,D,C,A,B,A}上,由LCS_LENGTH计算出的表c和b。第i行和第j列中的方块包含了c[i,j]的值以及指向b[i,j]的箭头。在c[7,6]的项4,表的右下角为X和Y的一个LCS的 长度。对于i,j>0,项c[i,j]仅依赖于是否有xi=yi,及项c[i-1,j]和c[i,j-1]的值,这几个项都在c[i,j]之前计 算。为了重构一个LCS的元素,从右下角开始跟踪b[i,j]的箭头即可,这条路径标示为阴影,这条路径上的每一个“↖”对应于一个使xi=yi为一个 LCS的成员的项(高亮标示)。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。