Najdłuższy wspólny podciąg

Najdłuższy wspólny podciąg (NWP, ang. longest common subsequence[1]) – najdłuższy podciąg znaków, które występują w tej samej kolejności w dwóch porównywanych łańcuchach. Elementy podciągów nie muszą przy tym leżeć obok siebie (tym różni się ten problem od problemu najdłuższego wspólnego podłańcucha, ang. longest common substring[2]). Rozwiązanie tego problemu jest bardzo przydatne przy pisaniu programów mających na celu wykrycie zmian w dokumentach lub plikach, lub przy pisaniu programów służących do identyfikacji plagiatów.

Przykłady:

  • Dla ciągów abaabbaaa i babab ich NWP to baba i abab.
  • Dla ciągów POLITECHNIKA i TOALETA ich NWP to OLTA i OLEA.
  • Dla ciągów 123 oraz 543 ich NWP to 3.

Warto przy tym zaznaczyć, że implementacja rozwiązania problemu może dotyczyć zarówno ciągu rozumianego jako ciąg liter, wyrazów czy nawet akapitów.

Algorytm znajdujący tablicę długości NWP dwóch ciągów edytuj

Idea edytuj

Problem NWP dwóch ciągów   i   o długościach odpowiednio   i   może być rozwiązany za pomocą metody programowania dynamicznego.

Algorytm ten ma złożoność obliczeniową rzędu   gdzie   oraz   to długości ciągów   i   Algorytm ten tworzy tablicę dwuwymiarową C (n na m) taką, że wartość C[i][j] jest długością NWP podciągów   i   A więc po zakończeniu wypełniania tablicy   komórka   będzie zawierała wartość będąca długością NWP oryginalnych ciągów wejściowych   i  

Optymalne rozwiązanie podproblemów edytuj

Załóżmy, że w danym kroku algorytmu chcemy obliczyć wartość komórki   tj. długość NWP dla podciągów   i   Jeżeli   i   są identyczne na pozycjach odpowiednio   i     to należy włączyć ten wspólny znak do znalezionego wcześniej wspólnego podciągu. Wtedy:

 

ponieważ długością NWP ciągów   i   będzie długość NWP podciągów   i   plus jeden wspólny element z pozycji   i  

Jeżeli znaki   i   są różne, to obliczenie   sprowadza się do sprawdzenia, który ze wspólnych podciągów słów   i   oraz   i   jest dłuższy, i włączenie go do tablicy wynikowej.

 

Zatem algorytm wypełniania tablicy   można zapisać jako:

 

Stany początkowe edytuj

Do obliczenia tablicy   potrzeba jeszcze tylko tzw. stanów początkowych, tj. wartości początkowych rekurencjnych wzorów na   Z obserwacji, iż   będzie zawsze równe   (ponieważ długość NWP ciągu   znaków i ciągu pustego jest zawsze równa zero, ponieważ NWP jest wtedy ciągiem pustym) wynika, że   Analogiczne rozumowanie można przeprowadzić dla    

Algorytm w pseudokodzie edytuj

    for i := 0 to n do   // wypełnienie stanów początkowych
        C[i][0] := 0;
    for j := 0 to m do
        C[0][j] := 0;

    for i := 1 to n do
        for j := 1 to m do
            if A[i] = B[j] then
                C[i][j] := C[i-1][j-1] + 1  // znaleziono kolejny element NWP
            else
                C[i][j] := max(C[i-1][j], C[i][j-1]);

Po zakończeniu działania powyższej procedury komórka C[n][m] będzie zawierać długość NWP ciągów A i B.

Algorytm odtwarzający NWP edytuj

Mając gotową tablicę   długości wspólnych podciągów podciągów   i   można łatwo odtworzyć NWP.

Odtwarzanie najdłuższego wspólnego podciągu zilustrujmy przykładem. Weźmy wspomniane wcześniej wyrazy ‘abaabbaaa’ oraz ‘babab’. Tworzymy tablicę o wymiarach o jeden większych niż odpowiednie długości wyrazów. W tym przypadku będzie to tablica 10×6. Wypełniamy ją zgodnie z algorytmem, a więc pierwszy rząd i pierwsza kolumna, zgodnie z warunkami początkowymi, będzie się równała 0.

Ø a b a a b b a a a
Ø 0 0 0 0 0 0 0 0 0 0
b 0
a 0
b 0
a 0
b 0
Ø a b a a b b a a a
Ø 0 0 0 0 0 0 0 0 0 0
b 0 0 1
a 0
b 0
a 0
b 0

W tym miejscu dochodzimy do miejsca (komórka [3][2]), gdzie występuje taka sama litera w kolumnie i wierszu, stąd też komórka przyjmuje wartość zainkrementowanej komórki znajdującej się po skosie u góry z lewej [2][1], w komórce [4][2] spotyka się literka ‘a’ oraz ‘b’ więc przepisuje się wartość wyższą z komórki [3][2] lub [4][1], dochodząc analogicznie do komórki [6][2] gdzie znów ‘b’ spotyka się z ‘b’ inkrementowana jest wartość komórki [5][1], i wyniesie ona 1. Po wykonaniu analogicznie całego algorytmu otrzymujemy tabelę:

Ø a b a a b b a a a
Ø 0 0 0 0 0 0 0 0 0 0
b 0 0 1 1 1 1 1 1 1 1
a 0 1 1 2 2 2 2 2 2 2
b 0 1 2 2 2 3 3 3 3 3
a 0 1 2 3 3 3 3 4 4 4
b 0 1 2 3 3 4 4 4 4 4

Jak widać komórka [10][6] pokazuje nam rzeczywistą długość najdłuższego wspólnego podciągu, która w tym przypadku wynosi 4. Aby dowiedzieć się jaki to podciąg należy ‘przejść’ z komórki [10][6] do komórki [0][0] według następującego algorytmu:

  1. jeśli któraś komórka sąsiednia do tej w której jesteśmy (z lewej strony lub u góry) ma wartość identyczną, to przechodzimy do niej,
  2. jeśli jesteśmy w komórce [0][0] to zakończ,
  3. jeśli nie ma takiej, do ciągu odpowiedzi dopisujemy na początek literę odpowiadającą tej komórce, a następnie idziemy do komórki o wartości mniejszej o 1 (po skosie do góry i w lewo),
  4. jeśli nie jesteśmy w komórce [0][0] to przeskocz do punktu 1. w przeciwnym wypadku Zakończ.

W związku z tym, że w każdym kolejnym kroku można iść zarówno w lewo, jak i do góry, można uzyskać dwa, lub więcej podciągów (choć w większości przypadków wystarcza nam poznanie jednego).

Sposoby przejść zilustrują następujące tabelki:

Ø a b a a b b a a a
Ø 0 0 0 0 0 0 0 0 0 0
b 0 0 1 1 1 1 1 1 1 1
a 0 1 1 2 2 2 2 2 2 2
b 0 1 2 2 2 3 3 3 3 3
a 0 1 2 3 3 3 3 4 4 4
b 0 1 2 3 3 4 4 4 4 4

Jak widać wynikiem takiego podążania po tabeli jest podciąg ‘abab’.

Ø a b a a b b a a a
Ø 0 0 0 0 0 0 0 0 0 0
b 0 0 1 1 1 1 1 1 1 1
a 0 1 1 2 2 2 2 2 2 2
b 0 1 2 2 2 3 3 3 3 3
a 0 1 2 3 3 3 3 4 4 4
b 0 1 2 3 3 4 4 4 4 4<

Wynikiem takich przejść jest podciąg ‘baba’.

Optymalizacje algorytmu edytuj

Konstrukcja tego algorytmu sprawia, że czas potrzebny na rozwiązanie tego problemu wzrasta kwadratowo wraz ze zwiększaniem się porównywanych ciągów. Dlatego też warto stosować różnego rodzaju optymalizacje.

  • W przypadku ograniczonego dostępu pamięci i potrzebie wyznaczenia jedynie długości najdłuższego wspólnego podciągu, warto zamiast tworzenia macierzy prostokątnej o wymiarach   na   stworzyć jedynie dwa wektory o długości   w których przechowywany będzie jedynie poprzedni i aktualny wiersz.
  • Można także wyznaczyć NWP w czasie O(nm) i pamięci O(\min\{n,m\}), wzorując się na algorytmie Hirschberga.
  • W przypadku gdy porównywane są 2 teksty, których mamy pewność, że na przykład początek i koniec są takie same, możemy je pominąć w analizie.
  • Zwykle najwięcej czasu w tym algorytmie zabiera porównywanie. Dlatego działania w kierunku zmniejszenia ilości porównań mogą być pożądane. Poszukiwanie najdłuższego wspólnego podciągu pod względem całych wyrazów, linii lub akapitów może być szybsze w stosunku do przeszukiwania poszczególnych liter. Jednak trzeba tu uważać, bo porównywanie ciągów znakowych pod względem identyczności też nie jest zbyt szybkie. W celu przyspieszenia tych porównań można sprowadzić je do porównań sum kontrolnych tych ciągów, co powinno skrócić długość porównywanych ciągów. Niestety trzeba się tu liczyć z tym, że mogą wystąpić kolizje sum kontrolnych i różne ciągi znakowe mogą być reprezentowane przez tę samą sumę kontrolną. Oprócz tego, przygotowanie sum kontrolnych może też być czasochłonne.
  • Zastosowanie innego algorytmu, skuteczniejszego dla tekstów mniej podobnych do siebie. Polega on na tym, że dla ciągów A oraz B tworzy się macierz   o liczbie kolumn równej długości krótszego ciągu, taką że:
    •   gdzie   to numer kolumny, a   to numer wiersza, jeśli istnieje to ma wartość:   gdzie   jest to najmniejsza liczba jaka spełnia warunki   oraz  
    • Istnieją dodatkowe warunki optymalizujące:
      •   dla i<k, czyli nie istnieje.
      • Jeżeli   nie istnieje oraz j nie istnieje to   nie istnieje.
      • Jeżeli   nie istnieje to   nie istnieje.
      • Jeżeli   to  
      • Jeżeli cały wiersz   to koniec algorytmu
    • Przykład działania: dane są ciągi POLITECHNIKA i TOALETA. Tworzymy macierz L o liczbie kolumn równej 7.
T O A L E T A
k=1 5 2 2 2 2 2 2
k=2 12 3 3 3 3
3 6 5 5
4 12
5
  • – oznacza komórki macierzy   które są null,
  • długość rozwiązania jest reprezentowana przez liczbę wierszy macierzy które nie są w całości null,
  • na niebiesko zaznaczone są numery znaków ciągu POLITECHNIKA które są najdłuższym wspólnym podciągiem, w tym przypadku OLTA.

Przypisy edytuj

  1. Tłumaczenie nazwy według Bartek Nowowiejski: LCS – najdłuższy wspólny podciąg. [dostęp 2009-07-12].
  2. Tłumaczenie nazwy według Jerzy Wałaszek: Wyszukiwanie najdłuższego wspólnego podłańcucha. [dostęp 2009-07-12]. [zarchiwizowane z tego adresu (17 listopada 2009)].