算法学习-最长公共子序列(LCS)

来源:互联网 发布:淘宝抢购怎么抢那么快 编辑:程序博客网 时间:2024/05/22 08:10

定义

  • 一个序列S任意删除若干个字符得到新序列T,则T焦作S的子序列;
  • 两个序列X和Y的公共子序列中,长度最长的那个,定义为X和Y的最长公共子序列
    • 字符串13455与245576的最长公共子序列为455
    • 字符串acdfg与adfc的最长公共子序列为adf
  • 注意区别最长公共子串,最长公共子串是要求连续的

分析
  • 暴力求解:穷举法
    • 假定字符串X,Y的长度分别为m,n
    • X的一个子序列即下标序列{1,2,...,m}的严格递增子序列,因此,X共有2的m次方个不同子序列;同理,Y有2的n次方个不同子序列,从而穷举搜索法需要只输时间O(2m方*2n方);
    • 对X的每一个子序列,检查它是否也是Y的子序列,从而确定它是否为X和Y的公共子序列,并且检查过程中选出最长的公共子序列;
    • 显然,这种方法不可取。
  • 合适的方法
    • 字符串X,长度为m,从1开始数;
    • 字符串Y,长度为n,从1开始数;
    • Xi=<x1,...,xi>即X序列的前i个字符(1<=i<=m)(Xi不放读作“字符串X的i前缀”)
    • Yj=<y1,...,yj>即Y序列的前j个字符(1<=j<=n)(Yj不放读作“字符串Y的j前缀”)
    • LCS(X,Y)为字符串X和Y的最长公共子序列即Z=<z1,...,zk>。
      • 注:不严格的表述。事实上,X和Y的可能崔仔多个子串,长度相同并且最大,因此,LCS(X,Y)严格的说,是个字符串集合。即:Z属于LCS(X,Y);
    • 若Xm=Yn(最后一个字符相同),则Xm与Yn的最长公共子序列Zk的最后一个字符必定为Xm(=Yn)。
      • zk=xm=yn
      • LCS(Xm,Yn)=LCS(Xm-1, Yn-1)+xm
    • 由上面的可以得到LCS(Xm,Yn)=W+xm,则W是Xm-1的子序列;同理,W是Yn-1的子序列,因此,W是Xm-1和Yn-1的公共子序列。
    • 举例:

      • 对于上面的字符串X和Y:
      • x3=y3=‘C‘,则LCS(BDC,ABC)=LCS(BD,AB)+‘C’
      • x5=y4=‘B’,则LCS(BDCAB,ABCB)=LCS(BDCA,ABC)+‘B’
    • 若xm !=yn,则:
      • 要么:LCS(Xm,Yn)=LCS(Xm-1,Yn)
      • 要么:LCS(Xm,Yn)=LCS(Xm,Yn-1)
      • 即LCS(Xm,Yn)=max{LCS(Xm-1,Yn),LCS(Xm,Yn-1)}
      • 对于上面的表格举个例子:
      • x2!=y2,则LCS(BD,AB)=max{LCS(BD,A), LCS(B,AB)}
      • x4!=y5,则LCS(BDCA,ABCBD)=max{LCS(BDCA,ABCB),LCS(BDC,ABCBD)}
    • 最终可以的到如下图所示的状态转移方程

    • 显然,属于经典的动态规划问题
分析完成,开始讨论代码如何实现
首先分析一下算法中的数据结构:长度数组
  • 使用二维数组C[m][n]
  • c[i][j]记录序列Xi和Yj的最长公共子序列的长度
    • 当i=0或j=0时,空序列是Xi和Yj的最长公共子序列,故c[i][j]=0。所以可以得到如下方程

实例如下:




我们可以根据状态转移方程和初始确定的值从左上角开始逐个往右下进行分析,可得到群体的最优解,所以这是经典的动态规划。
至于获得最大子串的方法,从图中右下角开始向上找,相等跳跃到坐上对角,不相等跳跃到左边或上边大的一格。


具体代码下面给出

// suanfa1.cpp : 定义控制台应用程序的入口点。//#include "stdafx.h"#include <iostream>#include <vector>void reverse(std::string::iterator iterbeg, std::string::iterator iterend){while(iterbeg < iterend){char tmp = *iterbeg;*(iterbeg++) = *iterend;*(iterend--) = tmp;}}void LCS(const char* str1, const char* str2, std::string& str){int size1 = (int)strlen(str1);int size2 = (int)strlen(str2);// 从1开始数,方便后面的代码编写// 这里减个1只所以不用担心内存问题是因为下面就没有去用s1[0]这个内存空间const char* s1 = str1 - 1;    const char* s2 = str2 - 1;std::vector<std::vector<int>> chess(size1 + 1, std::vector<int>(size2 + 1));int i, j;for (i = 0; i < size1; i++){chess[i][0] = 0;}for (j = 0; j < size2; j++){chess[0][j] = 0;}for (i = 1; i <= size1; i++){for (j = 1; j <= size2; j++){if (s1[i] == s2[j]){chess[i][j] = chess[i-1][j-1] + 1;}else{chess[i][j] = chess[i][j-1] ? chess[i][j-1] : chess[i-1][j];}}}i = size1;j = size2;while ((i != 0) && (j != 0)){if (s1[i] == s2[j]){str.push_back(s1[i]);i--;j--;}else{if (chess[i][j-1] > chess[i-1][j]){j--;}else{i--;}}}reverse(str.begin(), str.end() - 1);}int _tmain(int argc, _TCHAR* argv[]){const char* str1 = "TCGGATCGACTT";const char* str2 = "AGCCTACGTA";std::string str;LCS(str1, str2, str);std::cout<<str.c_str()<<std::endl;system("pause");return 0;}
LCS的应用:最长递增子序列LIS
找出给定数组最长且单调递增的子序列。
如:给定数组A{5,6,7,1,2,8},则其最长的单调递增子序列为{5,6,7,8},长度为4
其实此问题可以转换成最长公共子序列问题只需要将原始数组进行排序A’{1,2,5,6,7,8}
因为,原始数组A的子序列顺序保持不变,而且排序后A’本身就是递增的,这样,就保证了两个序列的最长公共子序列的递增特性。如此,若想求数组A的最长递增子序列,其实就是求数组A与它的排序数组A’的最长公共子序列
此外,本体也可以直接使用动态规划/贪心法来求解

0 0
原创粉丝点击