Problem
Znalezienie najdłuższego wspólnego podciągu dwóch ciągów x[1…m], y[1…n].
Przykład
Mamy x=(A,B,C,B,D,A,B) oraz y=(B,D,C,A,B,A).
Najdłuższym wspólnym podciągiem tych ciągów są BDAB, BCAB oraz BCBA.
Brute force
Dla każdego podciągu ciągu x sprawdź czy taki istnieje w ciągu y i zapamiętaj najdłuższy do tej pory znaleziony.
Liczba możliwych podciągów ciągu x o długości m to 2m. Sprawdzenie czy jakiś podciąg x występuje w y długości n zajmuje O(n), zatem złożoność obliczeniowa takiego algorytmu to O(n2m) — 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)∣, a następnie podamy procedurę odtworzenia LCS(x,y).
Niech c(i,j)=∣LCS(x[1…i],y[…j])∣ będzie długością najdłuższego wspólnego podciągu prefiksów ciągów x oraz y. Dla każdego i=1,…,m oraz j=1,…,n:c(i,j) będą pod-problemami, a c(m,n) będzie długością najdłuższego wspólnego pociągu ciągów x oraz y.
Zauważmy, że c(i,j)=⎩⎪⎪⎨⎪⎪⎧0c(i−1,j−1)+1max{ c(i−1,j), c(i,j−1) }i=0∨j=0x[i]=y[j]otherwise 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,…,xm⟩ and Y=⟨y1,y2,…,yn⟩ be sequences and let Z=⟨z1,z2,…,zk⟩ be any LCS of X and Y.
A prefix of A is noted as At. At contains first t elements of A.
- If xm=yn, then zk=xm=yn and Zk−1 is an LCS of Xm−1 and Yn−1.
- If xm=yn, then zk=xm implies that Z in an LCS of Xm−1 and Y.
- If xm=yn, then zk=yn implies that Z is an LCS of X and Yn−1.
Proof:
- If zk=xm, then we could append xm=yn to Z to obtain a common subsequence of X and Y of length k+1, contradicting the supposition that Z is a longest common subsequence of X and Y. Thus, we must have zk=xm=yn. Now, the prefix Zk−1 is a length-(k−1) common subsequence of Xm−1 and Yn−1. We wish to show that is in an LCS. Suppose for the puprose of contradiction that there exists a common subsequence W of Xm−1 and Yn−1 with length greater than k−1. Then appending xm=yn to W produces a common subsequence of X and Y whose length is greater than k, which is a contradiction [with the supposition that Z is the LCM of X and Y].
- If zk=xm, then Z is a common subsequence of Xm−1 and Y. If there were a common subsequence W of Xm−1 and Y with length greater than k, then W would also be a common sequence of Xm and Y, contradicting assumption is an LCS of X and Y.
- The proof is symmetric to
2.
.
Kolejność pod-problemów
Fakt ten określa nam kolejność pod-problemów c(i−1,j−1), c(i−1,j), c(i,j−1) są w porządku przed problemem 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) i krawędziami c(i−1,j−1)→c(i,j), c(i,j−1)→c(i,j) oraz c(i−1,j)→c(i,j) dla i=0,…,m oraz j=0,…,n.
Algorytm obliczający długość najdłuższego wspólnego podciągu sprowadza się do zaimplementowania obliczania 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(n⋅m): mamy n⋅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) musimy zapamiętać z którego pod-problemu korzystamy: c(i−1,j−1), c(i−1,j), c(i,j−1). Następnie po obliczeniu wszystkich c(i,j) zaczynamy przechodzić po naszym DAG-u rozpoczynając od wierzchołka c(m,n) według następujących zasad:
- jeśli rozwiązując c(i,j) użyliśmy c(i−1,j−1) to dodajemy x[i] (tożsame z y[j]) do naszego rozwiązanie i przechodzimy do c(i−1,j−1)
- jeśli rozwiązując c(i,j) użyliśmy c(i,j−1) to przechodzimy do c(i,j−1) (nie dodajemy nic do rozwiązania)
- jeśli rozwiązując c(i,j) użyliśmy c(i−1,j) to przechodzimy do c(i−1,j) (nie dodajemy nic do rozwiązania)
- kończymy jeśli i=0∨j=0.