算法之最长公共子序列(LCS)问题
算法课上老师留的作业,最长公共子序列LCS(Longest Common Subsequence)问题,首先看到这个问题感觉有点复杂,和最长公共子串不同,公共子序列并不要求元素相邻,看起来只有穷举才能将所有可能全部罗列,但是伟大的数学家早已为此提供了更优的解决方案——动态规划。
一、问题描述与分析
子序列的定义:一个给定序列的子序列是在该序列中删去若干元素后得到的序列。
而最长公共子序列的定义是,一个序列 S ,如果分别是两个或多个已知序列的子序列,且是所有符合此条件序列中最长的,则 S 称为已知序列的最长公共子序列。
若采用穷举搜索法,即对序列X的每个子序列检查是否也是序列Y的子序列,并在检查中选出最长的公共子序列,若X有m个元素,Y有n个元素,则X有2^m个子序列,算法的时间复杂度为O(2^(m+n))。
那么如果使用动态规划方法来解决这个问题呢?
首先:什么样的问题适合用dp来解呢?能采用动态规划求解的问题的一般要具有3个性质:
(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。
(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
所有求解dp问题的基本步骤有哪些呢?
求解的基本步骤
动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。
初始状态→│决策1│→│决策2│→…→│决策n│→结束状态
(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两个阶段的状态之间的关系来确定决策方法和状态转移方程。
(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
一般,只要解决问题的阶段、状态和状态转移决策确定了,就可以写出状态转移方程(包括边界条件)。
实际应用中可以按以下几个简化的步骤进行设计:
(1)分析最优解的性质,并刻画其结构特征。
(2)递归的定义最优解。
(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值
(4)根据计算最优值时得到的信息,构造问题的最优解
建立一个c[i,j]来记录Xi 和Yj的最长公共子序列的长度。
当i=0或j=0时,c[i,j]=0(Xi 和Yj 为空子序列);
当i,j>0且Xi =Yj 时,c[i,j]=c[i-1,j-1]+1;
当i,j>0且Xi ≠Yj 时,c[i,j]=max(c[i,j-1],c[i-1,j])。
二、求解算法
1、计算最优值
按以上关系可以轻松的写出求解c[i,j]的算法,X、Y序列为输入,二维数组c[i][j]为输出
1 void LCS_length(const string x,const string y){ 2 const int m = x.size(); 3 const int n = y.size(); 4 int c[100][100]; //生成二维数组c 5 for(int i = 0;i <= m; ++i) //i =0 和j = 0时c = 0 6 c[i][0] = 0; 7 for(int j = 1;j <= n; ++j) 8 c[0][j] = 0; 9 for(int i = 1;i <= m; ++i){ //x[i] = y[i]时 c[i][j] =c[i-1][j-1]+1 10 for(int j = 1; j<=n; ++j){ //x[i]不等于y[j]时c[i][j] = max(c[i,j-1],c[i-1,j] 11 if(x[i-1] == y[j-1]){ 12 c[i][j] = c[i-1][j-1]+1; 13 } 14 else if(c[i-1][j] >= c[i][j-1]){ 15 c[i][j] = c[i-1][j]; 16 } 17 else{ 18 c[i][j] = c[i][j-1]; 19 } 20 } 21 } 22 for(int i = 0;i <= m; ++i){ //输出二维数组c 23 for(int j = 0; j<=n; ++j){ 24 cout<<c[i][j]<<" "; 25 } 26 cout<<'\n'; 27 } 28 } 29 int LCS(const string x,const string y,int i,int j){ //递归求最优值 30 if(i==0||j==0) 31 return 0; //相当于用c[i-1,j-1],c[i-1,j],c[i,j-1] 32 else if(x[i-1]==y[j-1]) //求解c[i,j] 33 return LCS(x,y,i-1,j-1)+1; 34 else 35 return max(LCS(x,y,i-1,j),LCS(x,y,i,j-1)); 36 }
求解最优值写了两个版本,LCS_length 会生成二维数组用来求最优解,LCS用递归直接求值。
下为"abcbdab","bdcaba"的c[i][j]
分析出四种情况:(从下到上)
1、当c[i-1][j]=c[i-1][j-1]=c[i][j-1]=c[i][j]-1时,c[i][j]跳到c[i-1][j-1],并把x[i]加入子串的前面;
2、3、4、其他情况,c[i][j]跳到和其相等的上位或左位,若有上和左相等,则递归两次。
从右下角开始,由此得到下图
完整代码如下
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <cstdio> 5 using namespace std; 6 7 static int cont = 0;//定义静态变量记共有几种可能 8 void join(int c[100][100],int i,int j,string s,string x){ //可求出所有可能 9 if(i==0||j==0){ //到边上停止 10 cout<<s<<endl; //输出字符串 11 ++cont; //计数加一 12 return; 13 } 14 if(c[i-1][j]==c[i-1][j-1]&&c[i][j-1]==c[i-1][j-1]&&c[i][j]-1==c[i-1][j-1]){ 15 s =x[i-1]+s; //c[i-1][j]=c[i-1][j-1]=c[i][j-1]=c[i][j]-1 时子序列加一 16 join(c,i-1,j-1,s,x); 17 } 18 else if(c[i][j] == c[i-1][j]&&c[i][j]!=c[i][j-1]) 19 join(c,i-1,j,s,x); //和上边数相等,向上递归 20 else if(c[i][j] != c[i-1][j]&&c[i][j] == c[i][j-1]) 21 join(c,i,j-1,s,x); //和左边数相等,向左递归 22 else{ 23 join(c,i,j-1,s,x); //两种可能,向上向左 24 join(c,i-1,j,s,x); 25 } 26 } 27 void LCS_length(const string x,const string y){ 28 const int m = x.size(); 29 const int n = y.size(); 30 31 int c[100][100]; //生成二维数组c 32 for(int i = 0;i <= m; ++i) //i =0 和j = 0时c = 0 33 c[i][0] = 0; 34 for(int j = 1;j <= n; ++j) 35 c[0][j] = 0; 36 for(int i = 1;i <= m; ++i){ //x[i] = y[i]时 c[i][j] =c[i-1][j-1]+1 37 for(int j = 1; j<=n; ++j){ //x[i]不等于y[j]时c[i][j] = max(c[i,j-1],c[i-1,j] 38 if(x[i-1] == y[j-1]){ 39 c[i][j] = c[i-1][j-1]+1; 40 } 41 else if(c[i-1][j] >= c[i][j-1]){ 42 c[i][j] = c[i-1][j]; 43 } 44 else{ 45 c[i][j] = c[i][j-1]; 46 } 47 } 48 } 49 string b = ""; 50 join(c,m,n,b,x); //递归求出所有最长公共子序列 51 } 52 int LCS(const string x,const string y,int i,int j){ //递归求最优值 53 if(i==0||j==0) 54 return 0; //相当于用c[i-1,j-1],c[i-1,j],c[i,j-1] 55 else if(x[i-1]==y[j-1]) //求解c[i,j] 56 return LCS(x,y,i-1,j-1)+1; 57 else 58 return max(LCS(x,y,i-1,j),LCS(x,y,i,j-1)); 59 } 60 61 62 int main() 63 { 64 65 string a = "abcbdab"; 66 string b = "bdcaba"; 67 const int m = a.size(); 68 const int n = b.size(); 69 cout<<"最长公共子序列长度为:"<<LCS(a,b,m,n)<<endl;//输出最优值 70 LCS_length(a,b); 71 cout<<"共"<<cont<<"种可能"<<endl; 72 getchar(); 73 return 0; 74 }
结果:
bdab
bcab
bcba
共3种可能
与上图相同。