javascript 最長公共子序列

最長公共子序列(Longest Common Subsequence LCS)是從給定的兩個序列X和Y中取出儘量多的一部分字符,按照它們在原序列排列的前後次序排列獲得。LCS問題的算法用途普遍,如在軟件不一樣版本的管理中,用LCS算法找到新舊版本的異同處;在軟件測試中,用LCS算法對錄製和回放的序列進行比較,在基因工程領域,用LCS算法檢查患者DNA連與鍵康DNA鏈的異同;在防抄襲系統中,用LCS算法檢查論文的抄襲率。LCS算法也能夠用於程序代碼類似度度量,人體運行的序列檢索,視頻段匹配等方面,因此對LCS算法進行研究具備很高的應用價值。html

基本概念

  1. 子序列(subsequence): 一個特定序列的子序列就是將給定序列中零個或多個元素去掉後獲得的結果(不改變元素間相對次序)。例如序列<A,B,C,B,D,A,B>的子序列有:<A,B>、<B,C,A>、<A,B,C,D,A>等。
  2. 公共子序列(common subsequence): 給定序列X和Y,序列Z是X的子序列,也是Y的子序列,則Z是X和Y的公共子序列。例如X=[A,B,C,B,D,A,B],Y=[B,D,C,A,B,A[,那麼序列Z=[B,C,A]爲X和Y的公共子序列,其長度爲3。但Z不是X和Y的最長公共子序列,而序列[B,C,B,A]和[B,D,A,B]也均爲X和Y的最長公共子序列,長度爲4,而X和Y不存在長度大於等於5的公共子序列。對於序列[A,B,C]和序列[E,F,G]的公共子序列只有空序列[]。
  3. 最長公共子序列:給定序列X和Y,從它們的全部公共子序列中選出長度最長的那一個或幾個。
  4. 子串: 將一個序列從最前或最後或同時刪掉零個或幾個字符構成的新系列。區別與子序列,子序列是能夠從中間摳掉字符的。cnblogs這個字符串中子序列有多少個呢?很顯然有27個,好比其中的cb,cgs等等都是其子序列

給一個圖再解釋一下:前端

image_1c3ofq79gg4f1u7sf3o40gls29.png-95.5kB

咱們能夠看出子序列不見得必定是連續的,連續的是子串。es6

問題分析

咱們仍是從一個矩陣開始分析,本身推導出狀態遷移方程。算法

首先,咱們把問題轉換成前端夠爲熟悉的概念,不要序列序列地叫了,能夠認爲是數組或字符串。一切從簡,咱們就估且認定是兩個字符串作比較吧。segmentfault

咱們重點留意」子序列「的概念,它能夠刪掉多個或零個,也能夠所有幹掉。這時咱們的第一個子序列爲空字符串(若是咱們的序列不是字符串,咱們還能夠 )!這個真是千萬要注意到!許多人就是看不懂《算法導論》的那個圖表,還有許多博客的做者不懂裝懂。咱們老是從左到右比較,固然了第一個字符串,因爲做爲矩陣的高,就垂直放置了。數組

x "" B D C A B A
""
A
B
C
D
A
B

假令X = "ABCDAB", Y="BDCABA",各自取出最短的序列,也就是空字符串與空字符串比較。LCS的方程解爲一個數字,那麼這個表格也只能填數字。兩個空字符串的公同區域的長度爲0.ide

x "" B D C A B A
"" 0
A
B
C
D
A
B

而後咱們X不動,繼續讓空字符串出陣,Y讓「B」出陣,很顯然,它們的公共區域的長度爲0. Y換成其餘字符, D啊,C啊, 或者, 它們的連續組合DC、 DDC, 狀況沒有變, 依然爲0. 所以第一行都爲0. 而後咱們Y不動,Y只出空字任串,那麼與上面的分析同樣,都爲0,第一列都是0.函數

x "" B D C A B A
"" 0 0 0 0 0 0 0
A 0
B 0
C 0
D 0
A 0
B 0

LCS問題與揹包問題有點不同,揹包問題還能夠設置-1行,而最長公共子序列由於有空子序列的出現,一開始就把左邊與上邊固定死了。單元測試

而後咱們再將問題放大些,此次雙方都出一個字符,顯然只有兩都相同時,纔有存在不爲空字符串的公共子序列,長度也理解數然爲1。測試

A爲"X", Y爲"BDCA"的子序列的任意一個

x "" B D C A B A
"" 0 0 0 0 0 0 0
A 0 0 0 0 1
B 0
C 0
D 0
A 0
B 0

繼續往右填空,該怎麼填?顯然,LCS不能大於X的長度,Y的從A字符串開始的子序列與B的A序列相比,怎麼也能等於1。

x "" B D C A B A
"" 0 0 0 0 0 0 0
A 0 0 0 0 1 1 1
B 0
C 0
D 0
A 0
B 0

若是X只從派出前面個字符A,B吧,亦便是「」,「A」, "B", "AB"這四種組合,前兩個已經解說過了。那咱們先看B,${X_1} == ${Y_0}, 咱們獲得一個新的公共子串了,應該加1。爲何呢?由於咱們這個矩陣是一個狀態表,從左到右,從上到下描述狀態的遷移過程,而且這些狀態是基於已有狀態累加出來的。如今咱們須要確認的是,如今咱們要填的這個格子的值與它周圍已經填好的格子的值是存在何種關係。目前,信息太少,就是一個孤點,直接填1。

x "" B D C A B A
"" 0 0 0 0 0 0 0
A 0 0 0 0 1 1 1
B 0 1
C 0
D 0
A 0
B 0

而後咱們讓Y多出一個D作幫手,{"",A,B,AB} vs {"",B,D,BD},顯然,繼續填1. 一直填到Y的第二個B以前,都是1。 由於到BDCAB時,它們有另外一個公共子序列,AB。

x "" B D C A B A
"" 0 0 0 0 0 0 0
A 0 0 0 0 1 1 1
B 0 1 1 1 1 2
C 0
D 0
A 0
B 0

到這一步,咱們能夠總結一些規則了,以後就是經過計算驗證咱們的想法,加入新的規則或限定條件來完善。

image_1c3vdki881bpc127h1rdu11tu128b9.png-22.9kB
Y將全部字符派上去,X依然是2個字符,經仔細觀察,仍是填2.

看五行,X再多派一個C,ABC的子序列集合比AB的子序列集合大一些,那麼它與Y的B子序列集合大一些,就算不大,就不能比原來的小。顯然新增的C不能成爲戰力,不是二者的公共字符,所以值應該等於AB的子序列集合。

× "" B D C A B A
"" 0 0 0 0 0 0 0
A 0 0 0 0 1 1 1
B 0 1 1 1 1 2 2
C 0 1
D 0
A 0
B 0

image_1c3tbtfjjmhm17n6gt5mkt1s0h9.png-27.8kB

而且咱們能夠肯定,若是兩個字符串要比較的字符不同,那麼要填的格子是與其左邊或上邊有關,那邊大就取那個。

若是比較的字符同樣呢,稍安毋躁,恰好X的C要與Y的C進行比較,即ABC的子序列集合{"",A,B,C,AB,BC,ABC}與BDC的子序列集合{"",B,D,C,BD,DC,BDC}比較,獲得公共子串有「」,B,D 。這時仍是與以前的結論同樣,當字符相等時,它對應的格子值等於左邊與右邊與左上角的值,而且左邊,上邊,左上邊老是相等的。這些奧祕須要更嚴格的數學知識來論證。

假設有兩個數組,A和B。A[i]爲A的第i個元素,A(i)爲由A的第一個元素到第i個元素所組成的前綴。m(i, j)爲A(i)和B(j)的最長公共子序列長度。

因爲算法自己的遞推性質,其實只要證實,對於某個i和j:

  m(i, j) = m(i-1, j-1) + 1 (當A[i] = B[j]時)

  m(i, j) = max( m(i-1, j), m(i, j-1) ) (當A[i] != B[j]時)

第一個式子很好證實,即當A[i] = B[j]時。能夠用反證,假設m(i, j) > m(i-1, j-1) + 1 (m(i, j)不可能小於m(i-1, j-1) + 1,緣由很明顯),那麼能夠推出m(i-1, j-1)不是最長的這一矛盾結果。

第二個有些trick。當A[i] != B[j]時,仍是反證,假設m(i, j) > max( m(i-1, j), m(i, j-1) )。

由反證假設,可得m(i, j) > m(i-1, j)。這個能夠推出A[i]必定在m(i, j)對應的LCS序列中(反證可得)。而因爲A[i] != B[j],故B[j]必定不在m(i, j)對應的LCS序列中。因此可推出m(i, j) = m(i, j-1)。這就推出了與反正假設矛盾的結果。

得證。

咱們如今用下面的方程來繼續填表了。

image_1c3vdljfs1g5p1s5lpppcln1s88m.png-36.7kB

程序實現

//by 司徒正美
function LCS(str1, str2){
        var rows =  str1.split("")
        rows.unshift("")
        var cols =  str2.split("")
        cols.unshift("")
        var m = rows.length 
        var n = cols.length 
        var dp = []
        for(var i = 0; i < m; i++){ 
            dp[i] = []
            for(var j = 0; j < n; j++){ 
                if(i === 0 || j === 0){
                    dp[i][j] = 0
                    continue
                }
              
                if(rows[i] === cols[j]){ 
                    dp[i][j] = dp[i-1][j-1] + 1 //對角+1
                }else{
                    dp[i][j] = Math.max( dp[i-1][j], dp[i][j-1]) //對左邊,上邊取最大
                }
            }
            console.log(dp[i].join(""))//調試
        } 
        return dp[i-1][j-1]
    }

LCS能夠進一步簡化,只要經過挪位置,省去新數組的生成

//by司徒正美
function LCS(str1, str2){
    var m = str1.length 
    var n = str2.length
    var dp = [new Array(n+1).fill(0)] //第一行全是0
    for(var i = 1; i <= m; i++){ //一共有m+1行
        dp[i] = [0] //第一列全是0
        for(var j = 1; j <= n; j++){//一共有n+1列
            if(str1[i-1] === str2[j-1]){ 
                //注意這裏,str1的第一個字符是在第二列中,所以要減1,str2同理
                dp[i][j] = dp[i-1][j-1] + 1 //對角+1
            } else {
                 dp[i][j] = Math.max( dp[i-1][j], dp[i][j-1]) 
            }
        }
    } 
    return dp[m][n];
}

打印一個LCS

咱們再給出打印函數,先看如何打印一個。咱們從右下角開始尋找,一直找到最上一行終止。所以目標字符串的構建是倒序。爲了不使用stringBuffer這樣麻煩的中間量,咱們能夠經過遞歸實現,每次執行程序時,只返回一個字符串,沒有則返回一個空字符串, 以printLCS(x,y,...) + str[i]相加,就能夠獲得咱們要求的字符串。

咱們再寫出一個方法,來驗證咱們獲得的字符串是否真正的LCS字符串。做爲一個已經工做的人,不能寫的代碼像在校生那樣,不作單元測試就放到線上讓別人踩坑。

//by 司徒正美,打印一個LCS
function printLCS(dp, str1, str2, i, j){
    if (i == 0 || j == 0){
        return "";
    }
    if( str1[i-1] == str2[j-1] ){
        return printLCS(dp, str1, str2, i-1, j-1) + str1[i-1];
    }else{
        if (dp[i][j-1] > dp[i-1][j]){
            return printLCS(dp, str1, str2, i, j-1);
        }else{
            return printLCS(dp, str1, str2, i-1, j);
        }
    }
}
//by司徒正美, 將目標字符串轉換成正則,驗證是否爲以前兩個字符串的LCS
function validateLCS(el, str1, str2){
   var re =  new RegExp( el.split("").join(".*") )
   console.log(el, re.test(str1),re.test(str2))
   return re.test(str1) && re.test(str2)
}

使用:

function LCS(str1, str2){
    var m = str1.length 
    var n = str2.length
    //....略,自行補充
    var s = printLCS(dp, str1, str2, m, n)
    validateLCS(s, str1, str2)
    return dp[m][n]
}
var c1 = LCS( "ABCBDAB","BDCABA");
console.log(c1) //4 BCBA、BCAB、BDAB
var c2 = LCS("13456778" , "357486782" );
console.log(c2) //5 34678 
var c3 = LCS("ACCGGTCGAGTGCGCGGAAGCCGGCCGAA" ,"GTCGTTCGGAATGCCGTTGCTCTGTAAA" );
console.log(c3) //20 GTCGTCGGAAGCCGGCCGAA

image_1c3v0n1qnhq1sqa36min2goi16.png-11.6kB

打印所有LCS

思路與上面差很少,咱們注意一下,在LCS方法有一個Math.max取值,這實際上是整合了三種狀況,所以能夠分叉出三個字符串。咱們的方法將返回一個es6集合對象,方便自動去掉。而後每次都用新的集合合併舊的集合的字任串。

//by 司徒正美 打印全部LCS
function printAllLCS(dp, str1, str2, i, j){
    if (i == 0 || j == 0){
        return new Set([""])
    }else if(str1[i-1] == str2[j-1]){
        var newSet = new Set()
        printAllLCS(dp, str1, str2, i-1, j-1).forEach(function(el){
            newSet.add(el + str1[i-1])
        })
        return newSet
    }else{
        var set = new Set()
        if (dp[i][j-1] >= dp[i-1][j]){
            printAllLCS(dp, str1, str2, i, j-1).forEach(function(el){
              set.add(el)
            })
        }
        if (dp[i-1][j] >= dp[i][j-1]){//必須用>=,不能簡單一個else搞定
            printAllLCS(dp, str1, str2, i-1, j).forEach(function(el){
              set.add(el)
            })
        }   
        return set
    } 
 }

使用:

function LCS(str1, str2){
    var m = str1.length 
    var n = str2.length
    //....略,自行補充
    var s =  printAllLCS(dp, str1, str2, m, n)
    console.log(s)
    s.forEach(function(el){
        validateLCS(el,str1, str2)
        console.log("輸出LCS",el)
    })
    return dp[m][n]
}
var c1 = LCS( "ABCBDAB","BDCABA");
console.log(c1) //4 BCBA、BCAB、BDAB
var c2 = LCS("13456778" , "357486782" );
console.log(c2) //5 34678 
var c3 = LCS("ACCGGTCGAGTGCGCGGAAGCCGGCCGAA" ,"GTCGTTCGGAATGCCGTTGCTCTGTAAA" );
console.log(c3) //20 GTCGTCGGAAGCCGGCCGAA

image_1c3v1al221m5l1cdlmui48vd831j.png-54.5kB

空間優化

使用滾動數組:

function LCS(str1, str2){
    var m = str1.length 
    var n = str2.length
    var dp = [new Array(n+1).fill(0)],now = 1,row //第一行全是0
    for(var i = 1; i <= m; i++){ //一共有2行
        row = dp[now] = [0] //第一列全是0
        for(var j = 1; j <= n; j++){//一共有n+1列
            if(str1[i-1] === str2[j-1]){ 
                //注意這裏,str1的第一個字符是在第二列中,所以要減1,str2同理
                dp[now][j] = dp[i-now][j-1] + 1 //對角+1
            } else {
                dp[now][j] = Math.max( dp[i-now][j], dp[now][j-1]) 
            }
        }
        now = 1- now; //1-1=>0;1-0=>1; 1-1=>0 ...
    } 
    return row ? row[n]: 0
}

危險的遞歸解法

str1的一個子序列相應於下標序列{1, 2, …, m}的一個子序列,所以,str1共有${2^m}$個不一樣子序列(str2亦如此,如爲${2^n}$),所以複雜度達到驚人的指數時間(${2^m * 2^n}$)。

//警告,字符串的長度一大就會爆棧
function LCS(str1, str2, a, b) {
      if(a === void 0){
          a = str1.length - 1
      }
      if(b === void 0){
          b = str2.length - 1
      }
      if(a == -1 || b == -1){
          return 0
      } 
      if(str1[a] == str2[b]) {
         return LCS(str1, str2,  a-1, b-1)+1;
      }
      if(str1[a] != str2[b]) {
         var x =  LCS(str1, str2, a, b-1)
         var y =  LCS(str1, str2, a-1, b)
         return x >= y ? x : y
      }
  }

參考連接

  1. http://blog.csdn.net/hrn1216/...
  2. https://segmentfault.com/a/11...
  3. https://www.cnblogs.com/ider/...
  4. http://www.cppblog.com/mysile...
相關文章
相關標籤/搜索