【算法】 求最长公共子序列

最长公共子序列

  算法这玩意儿我完全是外行,因为从头开始就不是这个专业的再加上从小就对逻辑性强的东西苦手。。所以一直没什么机会也没什么兴趣学。去年刚开始学习了python的那段时间曾经碰到过几个算法比较高级的问题,当时觉得果然这不是我的能力能驾驭的了的。。总之是先记录了下来,但是对于算法这块将来的拓展和进一步学习,其实我挺没信心的 = =

  问题:最长公共子序列问题(Longest Common Sequence)

  子序列是指一个字符串抽掉0到若干个字符后剩下的字符串,抽取的字符不一定相邻也不一定有固定位置。比如ACD,BC,E,DE等都是ABCDE的子序列的一种。所以说子序列和子字符串是不同的,子字符串要求这部分字符串原原本本出现在原字符串中,但子序列不然。而这里的LCS问题,就是寻找两个字符串的最长公共子序列(简称LCS)。比如ABCBDAB和BDCABA的LCS就是BCBA或BCAB,我们的目标是求出其中一个。

  算法描述:

  有字符串a,b,len(a)等于m,len(b)等于n。

  1. 如果a[m-1](即a中的最后一个字符)和b[n-1]相同,那么这一位一定也是LCS的最后一位。这个说法已经有点跳了,不过还是可以理解的。因为如果这位不是LCS最后一位那就意味着a[:m-1]与b[:n-1]的LCS和a与b的LSC相同,此时再为a[:m]与b[:n]同时加上相同的最后一位,因为是相同的所以LCS必然增加一位,这就和之前的矛盾了。(T-T心中五味杂陈啊,数学证明我已经很久没碰过了,没想到想了半天还是没能想出比较清晰有力的证明)

  这样的话,a与b的LCS长度就等于 a[:m-1]与b[:n-1]的LCS长度+1,由此问题转化为求a[:m-1]与b[:n-1]的LCS。

  2. 如果a[m-1]与b[n-1]不同,这意味着两个末位字符至少有一个不是LCS的末位字符。那么a与b的LCS长度应该等于max(a[:m-1]与b[:n-2]的LCS长度 , a[:m-2]与b[:n-1]的LCS长度)。不说是a[:m-1]与b[:n-1]的LCS是因为考虑到这两者的LCS再加上比如说a[m-1]之后有可能这个字符串仍然是真包含于b[:n-1]中,此时LCS的长度就可以再加一了。

  3. 根据两种情况的不同以此类推,把求两个长字符串的LCS问题一点点往前推,推成一个最小的问题开始解决

  具体实现:

  构建一个二维数组c。行列数分别是m+1和n+1,也就是说行标i最大可以 = m;列标j最大可以 = n。先把a和b各自前面加上一个空字符,然后i,j表示分别从a,b中取出两个子字符串a[:i]与b[:j],比较这两个字符串得到的LCS的长度为二维数组元素c[i][j]的值。数组中的每一个元素(或者形象点说每一格)在横向和纵向上还对应了两个字符串各自位置的一个字符。

  c[i][j]的求法如下,

    ①  当i或j等于0,表明其中一方是空字符串,则c[i][j] == 0

    ②  当a[i] == b[j],则c[i][j] = c[i-1][j-1] + 1 (见算法描述中的第一条)

    ③  当a[i] != b[j],则c[i][j] = max(c[i][j-1],c[i-1][j])(见算法描述中第二条)

  由此看来,每个数组元素c[i][j]都可能有三种来源,分别是0(当i或j等于0),左上角那格+1,以及上方和左方两值较大者。三种来源之中仅第二种,即左上格+1得来的值,代表这个位置的字符在两个字符串中是一样且符合LCS顺序的,它无疑是LCS中的一员。所以我们只要找出这个二维数组中那些值的来源是第二种的数组元素,并且从左上角到右下角串联起这些元素,相应地把这些元素所对应的字符也串联起来,得到的就是LCS了。仔细想想其实还蛮科学的。。

  顺便一提:python中二维数组生成方式可以用一句列表生成器来实现:[[None for j in range(cols)] for i in range(rows)]如此可以生成大小为rows*cols的二维数组。

  根据上面提到过的ABCBDAB和BDCABA的例子画出的数组:

  

  可以看到从最右下角开始往前追溯,每个红圈圈出来的都是所谓第二种来源的元素,然后他们在两个字符串中对应的字符也恰恰是一样的,把这些字符连起来就成了BCBA,一个LCS了。在此例中,最右下角的4默认是往上开始追溯了,但是实际上还可以往左走出一条不同的路线,此时得到的是另一个LCS即BCAB。

  实现的python代码贴在下面,以我有限的能力实在想不出什么好办法来,就傻傻地给每个数组元素再添加了一个寻源头的属性。然后先生成整个数组再从数组的末尾开始寻源头,最后把输出给重新反序打印:

sa = "BDCABA"
sb = "ABCBDAB"
c = [[None for j in range(len(sa)+1)] for i in range(len(sb)+1)]

def fill(sa,sb,square):
    result = []
    san = len(sa)
    sbn = len(sb)
    sa = " "+sa
    sb = " "+sb
    for i in range(sbn+1):
        for j in range(san+1):
            if i == 0 or j == 0:
                square[i][j] = (0,"N")
            elif sa[j] == sb[i]:
                square[i][j] = (square[i-1][j-1][0] + 1 , "T")
            elif sa[j] != sb[i]:
                square[i][j] = (square[i-1][j][0],"U") if square[i][j-1][0] <= square[i-1][j][0] else (square[i][j-1][0],"L")

def find(i,j,square):
    if square[i][j][1] == "T":
        result.append(sa[j-1])
        find(i-1,j-1,square)
    elif square[i][j][1] == "U":
        find(i-1,j,square)
    elif square[i][j][1] == "L":
        find(i,j-1,square)
    elif square[i][j][1] == "N":
        return

fill(sa,sb,c)
result = []
find(len(sb),len(sa),c)
print ‘‘.join(reversed(result))

   听闻以上这种方法在业界好像被称为动态规划,是一种常见的算法思想。

时间: 2024-08-26 23:42:31

【算法】 求最长公共子序列的相关文章

动态规划算法解最长公共子序列LCS问题

第一部分.什么是动态规划算法 ok,咱们先来了解下什么是动态规划算法. 动态规划一般也只能应用于有最优子结构的问题.最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似).简单地说,问题能够分解成子问题来解决. 动态规划算法分以下4个步骤: 描述最优解的结构 递归定义最优解的值 按自底向上的方式计算最优解的值   //此3步构成动态规划解的基础. 由计算出的结果构造一个最优解.   //此步如果只要求计算最优解的值时,可省略. 好,接下来,咱们

[algorithm]求最长公共子序列问题

最直白方法:时间复杂度是O(n3), 空间复杂度是常数 reference:http://blog.csdn.net/monkeyandy/article/details/7957263 /** ** [email protected] ** http://blog.csdn.net/MonkeyAndy **/ 首先介绍动态规划方法的相关知识 动态规划方法的基本思想: 分成若干个子问题,先求解子问题,然后根据子问题的解求得原问题的解.经分解得到的子问题往往不是互相独立的.可重复利用! 其核心思

HDU 1243 反恐训练营 (动态规划求最长公共子序列)

反恐训练营 Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others) Total Submission(s): 3040    Accepted Submission(s): 693 Problem Description 当今国际反恐形势很严峻,特别是美国"9.11事件"以后,国际恐怖势力更是有恃无恐,制造了多起骇人听闻的恐怖事件.基于此,各国都十分担心恐怖势力会对本国社会造

(hdu step 3.2.2)Common Subsequence(简单dp:求最长公共子序列的长度)

在写题解之前给自己打一下广告哈~..抱歉了,希望大家多多支持我在CSDN的视频课程,地址如下: http://edu.csdn.net/course/detail/209 题目: Common Subsequence Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) Total Submission(s): 976 Accepted Submission(s): 538   Probl

ACM/ICPC 之 最长公共子序列计数及其回溯算法(51Nod-1006(最长公共子序列))

这道题被51Nod定为基础题(这要求有点高啊),我感觉应该可以算作一级或者二级题目,主要原因不是动态规划的状态转移方程的问题,而是需要理解最后的回溯算法. 题目大意:找到两个字符串中最长的子序列,子序列的要求满足其中字符的顺序和字母在两个序列中都必须相同,任意输出一个符合题意的子序列 首先是最基本的最长公共子序列的状态转移问题: 这里的maxLen[i][j]数组的意思就是保存s1的前 i 个字符和s2的前 j 个字符匹配的状态. 举个例子:maxLen[3][6]即表明在s1的前3个字符和s2

LCS求最长公共子序列(DP)

动态规划并不是一种算法,而是一种解决问题的思路.典型的动态规划问题,如最长公共子序列(LCS),最长单调子序列(LIS)等. 动态规划分为四个步骤: 1.判断问题是否具有最优子结构 这里以LCS为例,X={x1,x2,...,xi}:Y={y1,y2,...,yj}.最长公共子序列Z={z1,z2,...,zk}: ①如果xi=yj,那么zk=xi=yj,且Zk-1是序列Xi-1和Yj-1的LCS: ②如果xi≠yj,那么zk≠xi:且Zk是序列Xi-1和Yj的LCS: ③如果xi≠yj,那么z

UVA 10635--Prince and Princess+nlgn求最长公共子序列

题目链接:点击进入 刚看到这题目还以为又碰到水题了,结果写了个O(n^2)的代码交上去超时了,才发现n有250*250那么大.后面在网上找到了一个nlgn求最长上升子序列的方法,才过了.这个nlgn算法的主要思想是将最长公共子序列转成最长上升子序列,然后用最长上升子序列nlgn的算法求解.更具体的解释可以参看这篇博文:最长公共子序列(nlogn) 代码如下: #include<iostream> #include<cstring> #include<cstdio> #i

字符串算法之最长公共子序列

最长公共子序列,即 longest common subsequence,LCS.一个字符串删掉任意字符后所形成的字符串,不要求连续,注意和最长公共子串的区别. LCS的应用:论文查重,图形相似度比较,基因序列比较等. 暴力求解: 分别求出X.Y串的子序列,而后进行搜索比较,容易得到该算法复杂度为O(2^m · 2^n ),显然不可取 动态规划: 设有两个字符串X[1....m],Y[1....n],求其最长公共子串 假设Xi Yi为两个字符串从1开始数的第i个字符,若xm=yn,则xm必在最长

算法学习 - 最长公共子序列(LCS)C++实现

最长公共子序列 最长公共子序列的问题很简单,就是在两个字符串中找到最长的子序列,这里明确两个含义: 子串:表示连续的一串字符 . 子序列:表示不连续的一串字符. 所以这里要查找的是不连续的最长子序列, 动态规划 这里为什么要使用动态规划可以说一下,简单来说动态规划是为了降低时间复杂度的一种算法,申请一个额外空间,来保存每一个步骤的结果,最后从这些结果中找到最优的解. 这里有个问题就是:一般来说,当前的最优解,只与当前时刻和上一时刻有关系,和其他时刻没有关系,这样才能让动态规划发生作用,降低复杂度