Longest Common Subsequence

程度★★ 難度★★

Longest Common Subsequence(LCS)

中文譯做「最長共同子序列」。在一堆sequence當中,每個sequence都有而且是最長的subsequence,就是LCS。

s1: 2 5 7 9 3 1 2
s2: 3 5 3 2 8

以上例來看,s1和s2的LCS是5 3 2。這個例子恰恰好只有一個LCS,然而LCS可能同時會有很多個的,就像LIS的情況一樣。

s1: a b c d b c e e a
s2: c a b d e f g a
s3: d c e a

以上例來看,s1和s2和s3的其中一個LCS是c e a,另一個LCS是d e a。

一個簡單的演算法

求出一群序列的LCS,是NP-hard問題。一個簡單的演算法是窮舉s1當中所有的subsequence,每枚舉一個subsequence,就去檢查s2...sN是否都有這個subsequence。

枚舉s1所有subsequence的時間複雜度為O(s1!),每次檢查需時O(s2)+...+O(sN)=O(s2+...+sN),所以總時間複雜度是O(s1! * (s2+...+sN))。

至於求出兩個序列的LCS,則可以在多項式時間求得答案。接下來將詳細討論求出兩個序列的LCS的演算法。

Longest Common Subsequence: Dynamic Programming

程度★★ 難度★★

分割問題的方法

要找出LCS,就得先體會到其分割問題的精妙之處。這裡開始介紹分割問題的方法。

現在有兩個sequence分別為s1、s2,現在要找出s1和s2的LCS。先將s1的最後一個元素拆出來,這個元素稱做e1,而剩餘的部分稱做sub1好了。這裡以一道式子來簡單表示出剛剛的拆法,就是s1 = sub1 + e1。同樣的s2 = sub2 + e2。

要找出s1和s2的LCS,就等於是找出sub1 + e1和sub2 + e2的LCS。現在來觀察sub1、e1、sub2、e2與LCS的關係吧!首先將所有情況粗略的分做四種:一、e1是LCS的一部份,而e2不是;二、e2才是LCS的一部份,而e1不是;三、e1和e2都不是LCS的一部份;四、e1和e2都是LCS的一部份。

第一種情況。如果e1是LCS的一部份,而e2不是,那麼對e1來說,在sub2一定要能找到一個元素,和e1相同,才可能形成包含了e1的LCS。以另外一個方向去思考,則可以說e2是沒有用的元素,要找LCS只需要去找sub2就可以了。以上可以歸納一個結論:s1和s2的LCS,其實就是s1和sub2的LCS,可以把e2排擠掉。

第二種情況。和第一種情況類似,s1和s2的LCS,其實就是sub1和s2的LCS。

第三種情況。如果e1和e2都不是LCS的一部份,那麼直接找sub1和sub2的LCS就好啦!

第四種情況。如果e1和e2都是LCS的一部份,再者e1和e2都在最尾端,所以這種情況下,e1和e2是一定會相等的!而且e1亦會是LCS的最後一個元素。既然e1、e2都肯定是LCS的最後一個元素了,那麼要找剩下的部份,只需要從sub1和sub2著手即可,如同第三種情況一般。可以歸納出一個結論:s1和s2的LCS,必定是sub1和sub2的LCS在尾端加上e1(也是e2)。

總合以上分析,可以得到一個recurrence如下。

LCS(s1, s2) =
 { LCS(sub1, s2) or LCS (s1, sub2) or LCS(sub1, sub2) , when e1 != e2
 { LCS(sub1, sub2) + e1                               , when e1 == e2

經過焠鍊簡化之後,可以變成這樣。

LCS(s1, s2) =
 { max( LCS(sub1, s2), LCS(s1, sub2) ) , when e1 != e2
 { LCS(sub1, sub2) + e1                , when e1 == e2

然後再來考慮一下初始值要怎麼設定。一個簡單的想法是:當s1或s2是空的時候,必定找不到LCS。所以當s1或s2是空的時候,讓它們的LCS算出來是空集合就可以了。

以上就是求得LCS的方法了。這個方法逐步削減了最尾端的元素,將原問題縮小以求得解。事實上,將最尾端的字給拆開來的方法,是相當常見的一種手法。要是遇到了類似的題目,不妨也試著將尾端的字拆開來,慢慢分析所有情況來求得解。

計算LCS的長度

下面這段程式碼可以算出LCS的長度。

別忘了DP的精神!在這裡我們建立了一個二維陣列array,而array[i][j]代表著s1的第一個到第i個元素所形成的sequence,以及s2第一個到第j個元素所形成的sequence,這兩個sequence的LCS長度。而array[0][x]和array[x][0]則分別代表著第一個sequence為空的、第二個sequence為空的的情形,當然這些情形下LCS長度都是0,所以array[0][x]和array[x][0]的值也會是0。

找出一個LCS

可以從array陣列中找到LCS。觀察每一個e1 == e2的情況,因為e1 == e2的時候,也正是LCS增長的時候,故e1、e2是使LCS增長的元素。

要找到LCS,可以使用一個二維陣列,來記錄每一格的結果,是由哪一格而來。從最後的結果開始往回追溯,要是發現了某個array[i][j]是由array[i-1][j-1] + 1而得到的,便知道這個時候的e1、e2是使LCS增長的元素,此時印出s1[i]或者s2[j]其中一個就可以了。

找出所有的LCS

方才的程式碼可以找出一個LCS。但是若要找出所有的LCS呢?

我想這是可能做得到的,不過得先讓紀錄的方法更詳細一點才行。若是有一格的答案,可以由上方、左方或者左上方同時求得,那麼就要將這些同時求得的情形全部記錄起來。往回追溯的時候,就要將所有的情形都追溯一遍,便可求出所有可能的LCS。目前就我所知,似乎無法在線性時間內找出所有的LCS,一知半解,不再多提。

節省記憶體空間

如果只想求出LCS的長度,而不需要求出LCS的話,那麼便有個節省記憶體空間的方法。計算array裡的某個格子的值,只需要上方、左方、左上方的格子。如果計算的順序是橫的一行一行計算,那麼其實只需要兩行的記憶體空間交錯使用就足夠了。更節省的方法,是特別紀錄左上方的值,如此只需要一行的記憶體空間,再加上一個變數的空間。

有不少題目可供練習。

UVa 111 531 10066 10192 10252 10405 10723

© 2010 tkcn. All rights reserved.

Longest Common Subsequence: Hirschberg's Algorithm

程度★★ 難度★

演算法

【待補文字】

Longest Common Subsequence: Hunt-Szymanski Algorithm

程度★★ 難度★★★

LCS問題轉換成二維LIS問題

從LCS問題的DP表格可以觀察到,LCS問題可以化作類似於LIS的問題:從兩序列中找出對應的相同元素,以位置數對表示;這些位置數對可以排出的最長嚴格遞增序列,即是兩序列的LCS。

【註:這個類似於LIS的問題,就像是二維版的LIS。不過「二維LIS」目前沒有正式定義。】

(1) A LCS problem:

  index: 0 1 2 3 4 5 6 7 8 9
  --------------------------
  s1:    a b a c d
  s2:    d b a a b c a

(2) All matched positions, with the matched character:

    a     a     a     a     a     a     b     b     c     d
  (0,2) (0,3) (0,6) (2,2) (2,3) (2,6) (1,1) (1,4) (3,5) (4,0)

(3) Here is the corresponding 2D table:

      d b a a b c a
    +---------------
  a |     * *     *
  b |   *     *
  a |     * *     *
  c |           *
  d | *

(4-1) { LCS == 2D LIS } example:

  LCS   :   a     b     a
  2D LIS: (0,2) (1,4) (2,6) 數對呈嚴格遞增,在表格中則是往右下走。

      d b a a b c a
    +---------------
  a |     x *     *
  b |   *     x
  a |     * *     x
  c |           *
  d | *

(4-2) Another { LCS == 2D LIS } example:

  LCS   :   b     a     c
  2D LIS: (1,1) (2,2) (3,5)  數對呈嚴格遞增,在表格中則是往右下走。

      d b a a b c a
    +---------------
  a |     * *     *
  b |   x     *
  a |     x *     *
  c |           x
  d | *

二維LIS問題轉換成一維LIS問題

首先將所有數對排序,規則是第一個維度由小到大排序,當第一個維度相等時,第二個維度由大到小排序。排序之後,第二個維度的1D LIS,就對應到原數對們的2D LIS。

(1) 先將所有數列排序。
    第一維由小到大,當第一維相等時,第二維由大到小。
  (排序規則亦可改成以第二個維度為主,最後則是找第一個維度的1D LIS。)

    a     a     a     b     b     a     a     a     c     d
  (0,6) (0,3) (0,2) (1,4) (1,1) (2,6) (2,3) (2,2) (3,5) (4,0)

(2) 依序取出第二個維度的數值。

  6 3 2 4 1 6 3 2 5 0

(3) 這個序列的1D LIS,對應到數對們的2D LIS。其實也就是一開始要求的LCS。

  6 3 2 4 1 6 3 2 5 0
      * *   *

  1D LIS:   2     4     6
  2D LIS: (0,2) (1,4) (2,6)   (註:第一個維度之前有排序)
  LCS   :   a     b     a
  
(4) 簡潔的表達方式是:

     a b a c d                                s1字串
  -> { a(6,3,2) b(1,4) a(6,3,2) c(5) d(0) }   s1在s2當中出現的位置(由後往前)
  -> { 6 3 2 1 4 6 3 2 5 0 }                  依序取出第二個維度的數值
  -> 2 4 6                                    LIS

演算法

先把LCS問題看成二維LIS問題,再把二維LIS問題轉成一維LIS問題,然後用Robinson-Schensted-Knuth Algorithm來算LIS。

這裡以s1 = cbaa、s2 = abcaa為例。大意為:依序看s1的每個元素,是不是能讓目前的common subsequece變得更長。

  | s2:   | 目前可生成的    | 截至目前最理想的
  | abcaa | 最長共同子序列  | 最長共同子序列
--+-------+-----------------+-------------
c | ..c.. |  ..c..          | 3
--+-------+-----------------+-------------
b | .b... |  .b...          | 2
--+-------+-----------------+-------------
a | ....a |  .b..a          | 2 5
  | ...a. |  .b.a.          | 2 4
  | a.... |  a.... & .b.a.  | 1 4	// 同時紀錄到兩個
--+-------+-----------------+-------------
a | ....a |  a...a & .b.aa  | 1 4 5
  | ...a. |  a..a.          | 1 4 5
  | a.... |                 | 1 4 5	// 演算法結束

時間複雜度

先行判斷兩個序列的長度,將長度短的當作是s1,那麼時間複雜度就會是O(K * log(min(N,M))),其中K為位置數對的總數目,N和M分別是兩序列長度。也有人省略了min函數,直接寫成O(KlogN)。K最大可到N * M,遇到了極端的例子時,會跑得比用DP還慢。

實作的時候會利用counting sort的技巧,先掃描一遍s2,把s2每個字元的位置記下來,以符合時間複雜度。

計算LCS的長度(兩序列自身皆無重複元素)

計算LCS的長度

這裡再提供一個不使用binary search的程式碼,有時候會跑得比較快,各位可視情況採用之。

UVa 10635 10949

找出一個LCS、找出所有的LCS

可以參考LIS章節中的Robinson-Schensted-Knuth Algorithm。大家自己看著辦吧!

延伸閱讀:LIS問題與LCS問題可以互相轉換

LIS轉LCS:令原序列A排序後會變成B。A和B的LCS,就是A的LIS。

LCS轉LIS:將序列A和B當中的相同字母配對都找出來,呈現成索引值數對,再以特殊規則排序,最後找LIS,就是A和B的LCS。

Longest Common Subsequence: Four-Russians Speedup(Kronrod's Algorithm)

程度★★★ 難度★★

Four-Russians Speedup

http://par.cse.nsysu.edu.tw/paper/2004/041204/FourRussiansSpeedup.ppt

Longest Common Increasing Subsequence

程度★★ 難度★★

Longest Common Increasing Subsequence(LCIS)

http://comeoncodeon.wordpress.com/2010/06/01/longest-common-increasing-subsequence-lcis/

Longest Common Cyclic Subsequence

程度★★ 難度★★

Longest Common Cyclic Subsequence(LCCS)

Francois Nicolas and Eric Rivals. Longest common subsequence problem for unoriented and cyclic strings. Journal Theoretical Computer Science. Volume 370, Issue 1-3, 2007.