zoukankan      html  css  js  c++  java
  • 【算法】 求最长公共子序列

    最长公共子序列

      算法这玩意儿我完全是外行,因为从头开始就不是这个专业的再加上从小就对逻辑性强的东西苦手。。所以一直没什么机会也没什么兴趣学。去年刚开始学习了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))

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

  • 相关阅读:
    小程序(1)
    手机端放在线条中间的标题
    不定长度导航的两端对齐
    扇形导航菜单
    个性搜索框
    javascript数组原型方法
    jquery插件开发的demo
    监听表单中的内容变化
    mui中的关闭页面的几种方法
    css之伪类选择器:before :after(::before ::after)
  • 原文地址:https://www.cnblogs.com/franknihao/p/6822725.html
Copyright © 2011-2022 走看看