您的位置:首页 > 其它

算法学习之二——用DP和备忘录算法求解最长公共子序列问题

2014-11-16 09:54 274 查看
问题定义:

最长公共子序列:给定两个序列X={x1,x2,……xn},Y={y1,y2……,ym},如果X的子序列存在一个严格递增的下标序列{,,……},使得对于所有的j=1,2……,k,有=,则称产生的数组为对应的公共子序列。

如果公共子序列的长度最大,我们就称之为最长公共子序列,并求出LCS的长度(最优值)和对应的子序列(最优解)。

我们将和所对应的长度存储在数组里,并记录为c[i][j]。

我们可以证明问题的求解具有:

1.最优子结构性质。 问题的最优解包括子问题的最优解,不同子问题的最优解叠加在一起就是总问题的最优解

2.和重叠子问题:子问题有多个且具有重复性,如果两个序列不存在任何相同的元素,则c[1][1]=c[m]


基于以上两点性质,可以用动态规划的算法来进行求解。顺便一提,在ACM题目中有很多这样的题,任何求序列问题(如MaxSum)和树的问题(如二叉搜索树),几乎不用证明就可以去求解。

直接给出公式:

二 问题求解兼代码:

在已经给出公式的前提下,以下为所进行的的四种方法:

1.对穷举法: 我的思路是定义一个向量组Z,存储所有X元素存在的情况,|Z|=(2^n)*n,且Zn∈{0,1}。之后按照if(Zn,n∈1→n)的判断方式,每次都可以生成X的一个子序列,只要依次判断Y中是否存在公共子序列,存储并更新对应的长度,就能求出LCS的长度。但所耗费的时间复杂度太大。并且程序往往会卡住,运行不出来。

2.对直接递归法:在公式已经给定的情况下,最简单的思考方式就是进行直接递归。

我的伪代码如下:

<span style="font-size:14px;">Int Lcs_length(int i,int j){
If(i==0 || j==0) return 0;  //递归的边界条件
Else if(x[i-1]==y[j-1]) return Lcs_length(i-1,j-1)+1;  //每一次都要进行新的递归
Else return max(LCS_length(i,j-1),LCS_length(i-1,j)); }</span>


直接递归法的缺点是显而易见的,每一次进行计算的时候都要重复计算。比如我拿(6,6)来进行举例,(6,6)第一次递归后的情况是(6,5)和(5,6),两者分别进行递归后又是(5,5)、(6,4)和(5,5)、(4,6),也就是说(5,5)被重复计算了两次。如果存在大量不相等元素的话,就会因此产生大量的冗余,影响运行效率。

如果我们能够将每一次的结果(必要的)都保存下来,就可以节省大量的时间,因此导出了第三种方法。

3.备忘录法:备忘录实际上也是从上往下进行递归求解,只是每次都将求解的值记录下来,避免了大量的重复计算。

我的伪代码如下:

在直接递归法的基础上,只在代码的最后面加上return c[i][j],作为每一次递归调用的返回值。

其他情况下将return改为 c[i][j],记录在数组里即可。

具体实现如图: 其中p[100]本来是要求具体的序列的,但实现起来发现几乎不可能。

memset(c,-1,sizeof(c));
strcpy(x,"ABCBDAB");
strcpy(y,"BDCABA");
int m=strlen(x),n=strlen(y);
cout<<x<<endl;cout<<y<<endl;
cout<<"the length:"<<Memorized_LCS(m,n)<<endl;

count-=1;
while(count--) cout<<p[count]<<endl;
return 0;
}

int Memorized_LCS(int i,int j)
{   memset(c,-1,sizeof(c));
strcpy(x,"ABCBDAB");
strcpy(y,"BDCABA");
int m=strlen(x),n=strlen(y);
cout<<x<<endl;cout<<y<<endl;
if (c[i][j]>-1) return c[i][j]; //已经被计算过,就不用再次计算。
if(i==0 || j==0) c[i][j]=0; //边界条件
else if(x[i-1]==y[j-1])  {c[i][j]=Memorized_LCS(i-1,j-1)+1;p[count++]=x[i-1];}//因为不知道这次被调用的最终
//最终是否会被纳入总的结果,因此可以认为是无法求出序列的
else /*if(Memorized_LCS(i-1,j)> Memorized_LCS(i,j-1)) c[i][j]=Memorized_LCS(i-1,j);
else c[i][j]=Memorized_LCS(i,j-1);*/
c[i][j]=max(Memorized_LCS(i,j-1),Memorized_LCS(i-1,j)); //max是自带的函数
return c[i][j];

}


4.动态规划(DP)法:

动态规划法的思想同样来源于直接递归法,只不过提前就将每次求解需要用到的c[i-1][j-1]都提前求好。记录下来。

具体的求解过程如下所示:设两个子串的长度分别为n,m。则定义矩阵c[n+1][m+1],变化量从0到n,存储从子串x[1..n]和y[1..m]的子序列的长度。显然c[i][0]和c[0][j]都为0。

接下来按照c[1][1..m],c[2][1..m],……c
[1..m]的顺序自左向右来填每行的表。并且每一次递归时都记录下填表的方式,用1、2、3分别表示来自于c[i-1][j-1]、c[i-1][j]、c[i][j-1],这样在求出最优值的同时也能逆推出最优解。

代码如下:

#include<iostream>
#include<cstring>
using namespace std;
#define max 1000

int c[max][max];
char x[100],y[100],z[100];
int display[100][100];

int fillform(int n,int m){
//并没有进行递归调用
memset(c,0,sizeof(c));
memset(z,0,sizeof(z));
memset(display,0,sizeof(display));
int i,j,k;

//先填行和纵列,之后再每行每列的填表
for(i=1;i<=n;i++)
for(j=1;j<=m;j++){
if(x[i-1]==y[j-1]) {c[i][j]=c[i-1][j-1]+1;display[i][j]=1;}
else if(c[i][j-1]>c[i-1][j]) {c[i][j]=c[i][j-1];display[i][j]=2;}
else {c[i][j]=c[i-1][j];display[i][j]=3;} } //这时已经算出了所有的情况来
cout<<"test:"<<c[3][3]<<c
[m-1]<<endl;
return c
[m];
}

void show()
{

int n=strlen(x),m=strlen(y);
int i,j,k;

int count=0; //对应不同的作用
while(n>0 && m>0) //不能是>=0
{
if(display
[m]==1){
z[count++]=x[n-1];n--;m--;}
else if(display
[m]==2) m--; //向上移动一层
else if(display
[m]==3) n--; //向左移动一层
}

while(--count) cout<<z[count];cout<<z[0];
}

int main(void)
{
int count=5000;
strcpy(x,"ABCBDAB");
strcpy(y,"BDCABA");

//cout<<x[1]<<y[1]<<endl;
//cout<<strlen(x)<<" "<<strlen(y)<<endl;
cout<<x<<endl;cout<<y<<endl;
while(count--){
cout<<"the length:"<<fillform(strlen(x),strlen(y))<<endl;}
cout<<"the sequence:";show();

return 0;
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: