圖解最長公共子序列LCS問題

 

很多人在上學的時候都有過對畢業論文進行查重的經歷,一般可以通過CNKI,知網等平臺提交自己的論文,平臺將論文與其他論文進行匹配查重,最終得到一個相似度。不知大家對於查重問題有沒有思考過,他背後是如何實現的呢?難道就是找到相似的字符串匹配就可以嗎?本篇文章就帶大家瞭解一種可以解決這個問題的經典算法。

本文主要主要包含以下三方面:

  1. 什麼是LCS

  2. 如何實現LCS計算

  3. LCS的使用場景

 

1. 什麼是LCS

 

最長公共子序列,英文名稱爲Longest Common Subsequencem, 簡寫爲LCS。針對它的求解屬於動態規劃算法中的典型代表,算法主要的目的就是爲了求解兩個序列中最長的子序列,這裏很多人經常與最長公共子串問題搞混。我們後面通過一個實例來介紹下兩者的區別。

這裏我們首先要知道什麼是子序列, 假設有字符串“ABCBDAB“,針對該字符串的子序列既可以是相互連接在一起的:

  • ABC

  • ABCBD

  • BCBD

  • CBDA

  • ...

 

也可以是不連接在一起,但是保持前後順序的:

  • BBA

  • CDAB

  • BCAB

  • ACDB

  • ...

 

所以子序列這裏最關鍵的是前後順序,至於是否連接在一起則不重要。

如果是針對字符串相連接情況進行的算法處理,那是在求解最長公共子串,可以參考之前介紹的SuffixTrie結構來完成,在此不做過多介紹。

 

LCS的一個簡單的實例如下圖所示:兩個序列“ABCDE”和“ACED“他們的LCS爲圖中標記顏色的子序列,共同的子序列爲”ACD“, 這裏是不連續的但是在兩個序列中前後順序一致的序列。

 

 

這個序列比較短的情況下,我們通過觀察就可以發現,但是如何才能通過程序的方式表達出來呢,畢竟哪怕我們只有十幾個元素情況下,已經很難通過觀察發現其中的LCS了

 

2、如何計算LCS

 

如果我們通過暴力的方式來解這個問題,那麼需要多少計算量呢?我們先來看下子序列的個數。

  • 字符串長度是1,子序列只有一個,也就是字符串本身。

  • 字符串長度爲2,比如字符串AB,子序列的包含:A, B, AB三個。

  • 字符串長度爲3 ,比如字符串ABC, 子序列包含:A,B,C,AB,BC,AC,ABC七個。

結論:當長度爲N的時候,子序列的數量爲2的N次方-1 ,隨着長度的增加,其子序列的長度將呈現爲指數增加。對於數據量大的時候,這是非常不可取的一種計算方式。

關於LCS定義部分,我們提到了LCS屬於動態規劃算法問題,動態規劃的兩個典型的特徵是:

 

  1.  問題的最優解包含其子問題的最優解。

  2.  子問題空間有限,不會生成無限子問題,且存在後續計算上的重疊。

 

如果之前沒有接觸過動態規劃也沒有關係,稍後我們通過例子來慢慢介紹。

假設我們有兩個序列,需要求解LCS, 分別是下圖的{x1,x2...x(m)}序列和{y1,y2,...y(n)}序列,一個長度爲m, 一個長度爲n。爲了求解當前兩個序列的LCS, 我們可以將其拆分爲較短序列的子問題來解決,同時子問題也可以繼續向下拆分,直到序列爲空。

 

 

這裏我們推導一下計算子問題的流程:

(1)假設兩個序列的最後兩個值x(m)=y(n), 那麼序列的LCS實際上是原有的較小的子序列{x1,x2 .... x(m-1)} 和 {y1,y2, y3... y(n-1)}序列的LCS的值加上該相等值,因爲最後值相同且順序都在尾部,因此這個值必然屬於原LCS一部分,存在x(m)=y(n)=z(k1)。

舉個例子就可以看出來了,對於序列 “ABCD”和“ACED”兩個序列的LCS爲“ACD“, 這個時候尾部值相同。都去掉的話,相當於LCS也減少了一個值,因此當前序列LCS長度符合LCS[i, j ] = LCS(i-1, j-1) + 1

 

 

 

(2) 假設x(m+1) != y(n+1), 那麼新的序列LCS應該是兩對子序列的LCS最大值。表達式爲 LCS[i, j ] = max(LCS(i-1, j), LCS(j-1)) 。由於尾部的兩個值不相同,必然至少有一個是不被包含在LCS中或者兩個都不在LCS的。

我們還是通過一個實例看一下:對於序列 “ABCD”和“ACEF”兩個序列的LCS爲“AC“, 這個時候尾部的D和F值不相同,滿足上面的等式。

 

還有一種情況,去掉的值中包含了LCS一部分,比如序列”ABDC“和”ACEF“中尾部的兩個值不同,但是第一個序列中的C屬於LCS一部分。這時候仍舊滿足上面的等式:

 

 

 

 

用下面的表達式表示就是:

 

 

如果知道了序列的表達式,那我們在求解的過程中可以通過一些矩陣的方式來完成計算和追溯結果。

 

我們通過一個稍微複雜點的序列求解過程來介紹下如何通過一箇中間矩陣來求解上述的表達式。假設有序列A = ”ABCBDAB “ 和 序列 B= ”BDCABA“ , 我們首先初始化一個二維矩陣, 這個矩陣在處理的時候按照下面的規則處理:

 

  1. 二維矩陣的行高爲M+1, 其中M爲序列A的長度。列高爲N+1,其中N爲序列B的長度

  2. 如果矩陣中xi == yj , 則更新當前值爲對角值+1, 並填充一個”↖“

  3. 如果矩陣中xi != yj 則更新當前值爲上面的值和左面的值的最大值,並填充一個方向箭頭,指向最大值。但如果左邊的值和上面的值相同,則指向上面的值(此條件包含等於關係)

這裏我們逐行處理表格, 對於第一行的處理完成後,其中存在一個相同的值,這是我們更新值,對角值爲顏色標記爲紅色的方塊。按照上述規則處理完成後如下面的右圖所示。

 

依次處理完成後,整個的二維矩陣如下面所示:

 

 

創建該矩陣的程序如下面所示, 傳遞的參數爲兩個字符串數組,函數的目的爲了獲取上面的矩陣,結果返回兩個二維矩陣,第一個用於存儲箭頭方向,第二個用於存儲數值。

矩陣最右下角的值就是LCS的長度,這裏是4表示爲長度爲四個字母, 但是如何獲得這個LCS呢?這裏我們只需要跟隨箭頭的方向就可以獲取到最長的序列。跟隨的規則如下:

  1. 從最右下角開始進行矩陣遍歷

  2. 如果遇到一個向上的箭頭,則移動到上一層的方塊(綠色標記)

  3. 如果遇到一個向左的箭頭,則移動到左邊方塊(綠色標記)

  4. 如果遇到一個”↖“箭頭, 則該值爲LCS的一部分(紅色標記)。

 

 

回溯的程序代碼如下, 程序參數爲包含箭頭的二維數組(上面的程序返回的第一個值),數組A序列,以及i, j 分別代表序列長度,這裏是遞歸調用。第一次調用的時候傳遞的值i和j分別爲數組A和B的長度即可。

 

 

3. 典型的使用場景

 

(1) DNA序列本身可以看做是由「A, C, G, T」四個字符組成的字符串,可以使用LCS算法爲兩個DNA序列做相似度匹配, 查找到兩個DNA序列的LCS,可以知道兩者之間的差距。

(2) 論文相似度查重, 檢查兩篇論文中相似地方,從而發現是否有抄襲的嫌疑

(3) Git系統中代碼的比對算法Git-diff  

通過上面的介紹,或許大家對於LCS的求解有了一定的認識,也對於如何實現論文查重的實現有一定的理解了吧。 大家對於算法和日常使用感興趣的話,可以掃描下面的二維碼來訂閱我的個人公衆號,感謝大家的閱讀。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章