Longest Common Subsequence (LCS)

by Jerry Sky

2020-04-20



Problem

Znalezienie najdłuższego wspólnego podciągu dwóch ciągów x[1m], y[1n]x[1\dots m],~y[1\dots n].

Przykład

Mamy x=(A,B,C,B,D,A,B)x = (A,B,C,B,D,A,B) oraz y=(B,D,C,A,B,A)y = (B,D,C,A,B,A).
Najdłuższym wspólnym podciągiem tych ciągów są BDAB\text{BDAB}, BCAB\text{BCAB} oraz BCBA\text{BCBA}.

Brute force

Dla każdego podciągu ciągu xx sprawdź czy taki istnieje w ciągu yy i zapamiętaj najdłuższy do tej pory znaleziony.
Liczba możliwych podciągów ciągu xx o długości mm to 2m2^m. Sprawdzenie czy jakiś podciąg xx występuje w yy długości nn zajmuje O(n)O(n), zatem złożoność obliczeniowa takiego algorytmu to O(n2m)O(n2^m) — wykładnicza złożoność obliczeniowa.

Podejście programowania dynamicznego

Musimy zdefiniować pod-problemy, ich kolejność oraz relację pomiędzy nimi, co da nam DAG-a odpowiadającego problemowi LCS. Podobnie jak w poprzednio rozpatrywanych problemach, tutaj również zaczniemy od znalezienia długości najdłuższego wspólnego podciągu LCS(x,y)|\mathrm{LCS}(x,y)|, a następnie podamy procedurę odtworzenia LCS(x,y)\mathrm{LCS}(x,y).

Niech c(i,j)=LCS(x[1i],y[j])c(i,j) = |\mathrm{LCS}(x[1\dots i], y[\dots j])| będzie długością najdłuższego wspólnego podciągu prefiksów ciągów xx oraz yy. Dla każdego i=1,,mi = 1,\dots,m oraz j=1,,n:c(i,j)j = 1,\dots,n: c(i,j) będą pod-problemami, a c(m,n)c(m,n) będzie długością najdłuższego wspólnego pociągu ciągów xx oraz yy.

Zauważmy, że c(i,j)={0i=0j=0c(i1,j1)+1x[i]=y[j]max{ c(i1,j), c(i,j1) }otherwise c(i,j) = \begin{cases} 0 & i = 0 \lor j = 0\\ c(i - 1, j - 1) + 1 & x[i] = y[j]\\ \max\{~c(i-1, j),~c(i,j-1)~\} & \text{otherwise} \end{cases} Powyższy fakt ma dowód pochodzący z rozdziału 15.4 książki Introduction to algorithms by CLRS:

Theorem Optimal substructure of an LCS

Let X=x1,x2,,xmX = \langle x_1,x_2,\dots,x_m\rangle and Y=y1,y2,,ynY = \langle y_1,y_2,\dots,y_n\rangle be sequences and let Z=z1,z2,,zkZ = \langle z_1,z_2,\dots,z_k\rangle be any LCS of XX and YY.

A prefix of AA is noted as AtA_t. AtA_t contains first tt elements of AA.

  1. If xm=ynx_m = y_n, then zk=xm=ynz_k = x_m = y_n and Zk1Z_{k-1} is an LCS of Xm1X_{m-1} and Yn1Y_{n-1}.
  2. If xmynx_m \neq y_n, then zkxmz_k \neq x_m implies that ZZ in an LCS of Xm1X_{m-1} and YY.
  3. If xmynx_m \neq y_n, then zkynz_k \neq y_n implies that ZZ is an LCS of XX and Yn1Y_{n-1}.

Proof:

  1. If zkxmz_k \neq x_m, then we could append xm=ynx_m = y_n to ZZ to obtain a common subsequence of XX and YY of length k+1k+1, contradicting the supposition that ZZ is a longest common subsequence of XX and YY. Thus, we must have zk=xm=ynz_k = x_m = y_n. Now, the prefix Zk1Z_{k-1} is a length-(k1)(k-1) common subsequence of Xm1X_{m-1} and Yn1Y_{n-1}. We wish to show that is in an LCS. Suppose for the puprose of contradiction that there exists a common subsequence WW of Xm1X_{m-1} and Yn1Y_{n-1} with length greater than k1k-1. Then appending xm=ynx_m = y_n to WW produces a common subsequence of XX and YY whose length is greater than kk, which is a contradiction [with the supposition that ZZ is the LCM of XX and YY].
  2. If zkxmz_k \neq x_m, then ZZ is a common subsequence of Xm1X_{m-1} and YY. If there were a common subsequence WW of Xm1X_{m-1} and YY with length greater than kk, then WW would also be a common sequence of XmX_m and YY, contradicting assumption is an LCS of XX and YY.
  3. The proof is symmetric to 2..

Kolejność pod-problemów

Fakt ten określa nam kolejność pod-problemów c(i1,j1)c(i -1, j -1), c(i1,j)c(i-1,j), c(i,j1)c(i,j-1) są w porządku przed problemem c(i,j)c(i,j), oraz relację pomiędzy nimi.
Możemy zatem zauważyć, że DAG odpowiadający problemowi LCS jest kratą z wierzchołkami c(i,j)c(i,j) i krawędziami c(i1,j1)c(i,j)c(i-1,j-1) \rightarrow c(i,j), c(i,j1)c(i,j)c(i,j-1) \rightarrow c(i,j) oraz c(i1,j)c(i,j)c(i-1,j) \rightarrow c(i,j) dla i=0,,mi = 0,\dots,m oraz j=0,,nj = 0,\dots,n.

Algorytm obliczający długość najdłuższego wspólnego podciągu sprowadza się do zaimplementowania obliczania c(i,j)c(i,j) w porządku określonym przez stworzony DAG (zapamiętując rozwiązania pod-problemów) oraz wykorzystując relację pomiędzy pod-problemami opisaną w powyższym fakcie.

Pseudokod

LCS-Length(X,Y):
  m = X.length
  n = Y.length
  let b[1..m, 1..n] and c[0..,0..n] be new tables

  for i = 1 to m:
    c[i,0] = 0
  for j = 0 to n:
    c[0,j] = 0
  for i = 1 to m:
    for j = 1 to n:
      if x_i == y_j:
        c[i,j] = c[i-1, j-1] + 1
        b[i,j] = „↖”
      elif c[i-1, j] ≥ c[i, j-1]:
        c[i,j] = c[i-1,j]
        b[i,j] = „↑”
      else:
        b[i,j] = „←”

  return c and b

Source (Chapter 15.4)

Złożoność obliczeniowa tak zdefiniowanego algorytmu wynosi O(nm)O(n\cdot m): mamy nmn\cdot m pod-problemów, każdy rozwiązujemy w czasie stałym (wyniki to z relacji pomiędzy pod-problemami) jeśli mamy już rozwiązane pod-problemy, które występują wcześniej w określonym porządku.

Odtwarzanie LCS

W celu odtworzenia jednego ze zbioru najdłuższych wspólnych podciągów, obliczając c(i,j)c(i,j) musimy zapamiętać z którego pod-problemu korzystamy: c(i1,j1)c(i-1,j-1), c(i1,j)c(i-1,j), c(i,j1)c(i,j-1). Następnie po obliczeniu wszystkich c(i,j)c(i,j) zaczynamy przechodzić po naszym DAG-u rozpoczynając od wierzchołka c(m,n)c(m,n) według następujących zasad: