算法学习之二——用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.对直接递归法:在公式已经给定的情况下,最简单的思考方式就是进行直接递归。
我的伪代码如下:
直接递归法的缺点是显而易见的,每一次进行计算的时候都要重复计算。比如我拿(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]本来是要求具体的序列的,但实现起来发现几乎不可能。
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],这样在求出最优值的同时也能逆推出最优解。
代码如下:
最长公共子序列:给定两个序列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; }
相关文章推荐
- 算法(DP):最长公共子序列问题
- 【算法学习笔记】42.正反DP 填充问题 SJTU OJ 1285 时晴时雨
- 【算法导论学习-29】动态规划经典问题02:最长公共子序列问题(Longest common subsequence,LCS)
- 算法学习:子集和数问题求解
- 【算法系列学习】[kuangbin带你飞]专题十二 基础DP1 F - Piggy-Bank 【完全背包问题】
- 【算法学习】最大子数组问题的分治法求解
- 算法设计和数据结构学习_3(《数据结构和问题求解》part2笔记)
- 算法设计和数据结构学习_4(《数据结构和问题求解》part4笔记)
- 算法设计和数据结构学习_4(《数据结构和问题求解》part4笔记)
- 【算法导论学习-29】动态规划经典问题02:最长公共子序列问题(Longest common subsequence,LCS)
- 小白算法学习:求解两个字符串的最长公共子序列
- 算法学习---关于哈密顿图的哈密顿通路求解问题
- 算法学习 - 动态规划(DP问题)装配线问题(C++)
- 算法设计和数据结构学习_3(《数据结构和问题求解》part2笔记)
- 算法学习笔记----最长公共子序列问题
- 线性和并行求解多个序列最长公共子序列(MLCS)算法学习笔记
- 01背包问题求解(经典DP)(转)
- Trie树的应用,一道算法问题求解 代码实现
- Trie树的应用,一道算法问题求解 问题分析
- 数独求解——面向对象解决算法问题(一)