Najdłuższy wspólny podciąg: Python, C++ Przykład

⚡ Inteligentne podsumowanie

Najdłuższy wspólny podciąg identyfikuje najdłuższy uporządkowany wzorzec elementów wspólny dla dwóch ciągów znaków, bez konieczności stosowania ciągłych znaków. Ten klasyczny przykład programowania dynamicznego stanowi podstawę narzędzi różnicowych, dopasowania DNA i kontroli wersji poprzez efektywne porównywanie sekwencji w czasie wielomianowym.

  • 📘 Koncepcja główna: Najdłuższy wspólny podciąg zwraca najdłuższy uporządkowany zestaw znaków, który pojawia się w obu ciągach wejściowych, zachowując ich oryginalną względną kolejność.
  • 🐢 Naiwne podejście: Metoda siłowa polega na wyliczeniu każdego podciągu pierwszego ciągu i porównaniu go z drugim, przy czym czas działania metody wynosi wykładniczy O(n·2^m).
  • 🔁 Metoda rekurencyjna: Reguła rekurencyjna dopasowuje ostatnie znaki lub rekurencyjnie stosuje mniejsze podciągi, ale ponownie oblicza nakładanieping podproblemy wielokrotnie.
  • 🧮 Programowanie dynamiczne: Dwuwymiarowa tabela dp przechowuje w pamięci podręcznej wyniki podproblemów, dając czyste rozwiązanie O(m·n) z dodatkową przestrzenią O(m·n).
  • 🐍 Zakres języków: Absolutna Python oraz C++ implementacje przedstawiają zarówno rekurencyjną linię bazową, jak i zapamiętywaną tabelę dp do praktycznego wykorzystania.
  • 🌐 Rzeczywiste zastosowania: Najdłuższy wspólny podciąg obsługuje narzędzia porównywania, sprawdzania plagiatów, korekty pisowni i bioinformatyczne wyrównywanie sekwencji DNA i białek.

Najdłuższa wspólna kolejność

Jaki jest najdłuższy wspólny podciąg?

Najdłuższy wspólny podciąg (LCS) oznacza, że ​​otrzymasz dwa ciągi znaków, wzory lub sekwencje obiektów. Spośród tych dwóch ciągów znaków lub sekwencji musisz znaleźć najdłuższy podciąg elementów w tej samej kolejności, występujący w obu ciągach znaków lub wzorcach.

Przykład

Na przykład podano dwa ciągi znaków. Załóżmy, że:

Wzór_1 = „RGBGARGA”
Wzór_2 = „BGRARG”

  • Z wzorca pattern_1 można tworzyć sekwencje takie jak „RGB”, „RGGA”, „RGAR”. Aby utworzyć sekwencję, należy zachować względną pozycję każdego znaku w ciągu.
  • Z wzorca pattern_2 możemy generować sekwencje takie jak „BGR”, „BRAG”, „RARG”. Sekwencje mogą być generowane pod warunkiem, że zachowują względną pozycję oryginalnego ciągu znaków.

Termin pozycja względna oznacza porządek.

Na przykład „BRG” jest poprawną sekwencją, ponieważ w oryginalnym ciągu znaków pattern_2 najpierw pojawiło się „B”, potem „R”, a potem „G”. Jednak jeśli sekwencją jest „RBRG”, nie jest ona poprawna, ponieważ w oryginalnym ciągu znaków (pattern_2) najpierw pojawiło się „B”.

Przykłady najdłuższych wspólnych podciągów

Mamy dwie możliwości znalezienia najdłuższego wspólnego podciągu z podanych dwóch sekwencji lub tablic.

  • Naiwna metoda
  • Rozwiązanie do programowania dynamicznego: najdłuższy wspólny podciąg jest również znany jako LCS.

Rozwiązanie naiwne ma większą złożoność czasową i nie jest rozwiązaniem optymalnym. Korzystając z metody programowania dynamicznego (DP), rozwiązujemy problem złożoności.

Metoda naiwna

Metoda naiwna to proste podejście do problemu, niezależnie od złożoności czasowej i innych czynników optymalizacyjnych. W większości przypadków polega ona na „brutalnej sile”, wielokrotnych pętlach i wywołaniach rekurencyjnych. Termin „brutalna siła” oznacza przechodzenie przez wszystkie możliwe wzorce dla danego problemu.

Przykład

Na podstawie powyższego przykładu wzoru1 i wzoru2 załóżmy, że wzór1 ma długość m, a wzór2 ma długość n. Aby sprawdzić każdy możliwy przypadek, musimy ocenić każdy możliwy podciąg wzorca 1 za pomocą wzorca 2.

Oto prosty, czteroliterowy ciąg „ABCD”. Na przykład, musimy utworzyć sekwencję z „ABCD”. Możemy wybrać znak lub nie. Oznacza to, że dla każdego znaku mamy dwie możliwości:

  • Znak zostanie dodany do podciągu.
  • Znak nie zostanie dodany do podciągu.

Tutaj rysunki pokazują wszystkie sekwencje, które możemy ułożyć ze ciągu „ABCD”.

Sekwencje metody naiwnej ABCD

Sekwencja z 1 znakiem:

Metoda naiwna – sekwencje pojedynczych znaków

Sekwencje z 2 znakami:

Metoda naiwna – dwie sekwencje znaków

Sekwencje z 3 znakami:

Metoda naiwna – trzy sekwencje znaków

Z powyższego diagramu wynika, że ​​istnieje 14 sekwencji. Jeśli nie weźmiemy żadnych liter, czyli po prostu pustego ciągu, liczba sekwencji wyniesie 15. Co więcej, sam ciąg „ABCD” jest sekwencją. Zatem liczba sekwencji wynosi 16.

Zatem możliwe jest wygenerowanie 2^4 lub 16 podciągów z „ABCD”. Następnie ciąg o długości m będzie miał całkowity podciąg wynoszący 2^m.

Dla każdego podciągu musimy sprawdzić cały wzorzec2. Zajmie to O(n) czasu. O(n) oznacza funkcję złożoności obliczającą czas potrzebny na wykonanie.

Całkowita złożoność czasowa staje się zatem O(n*2^m). W przykładzie powyżej wartości m=8 i n=5.

Oto kroki metody naiwnej:

Krok 1) Weź sekwencję ze wzorca 1.
Krok 2) Dopasuj sekwencję z kroku 1 do wzorca 2.
Krok 3) Jeśli pasuje, zapisz podciąg.
Krok 4) Jeśli we wzorcu 1 pozostały jeszcze jakieś sekwencje, przejdź ponownie do kroku 1.
Krok 5) Wydrukuj najdłuższy podciąg.

Optymalna podkonstrukcja

Termin „optymalna podstruktura” oznacza, że ​​optymalne rozwiązanie można znaleźć, rozwiązując podproblemy. Na przykład, w powyższym przykładzie mamy wzorzec 1 i wzorzec 2.

Krok 1) Weź pierwsze dwa znaki z każdego wzoru.

Krok 2) Weź od trzeciego do piątego znaku z każdego wzoru.

Krok 3) Postępuj podobnie z pozostałymi znakami.

Struktura rekurencyjna problemu LCS

Struktura rekurencyjna problemu LCS

Znajdujemy LCS podciągu (ciągu wygenerowanego z ciągu oryginalnego). Następnie zapisujemy długość LCS podciągów.

Oto kolejna interesująca właściwość zakładkaping podproblemyMówi się, że problem ma nakładanie sięping podproblemy, jeżeli opis problemu można rozbić na małe podproblemy i użyć go kilkakrotnie w programie.

Poniższy diagram pokazuje, że algorytm rekurencyjny wywoływał funkcję z tym samym parametrem kilka razy.

Optymalne nakładanie się podstrukturyping podproblemy

Na przykład spójrz na drzewo rekurencji. W ciemnym polu możesz zauważyć nakładanie sięping podproblemy. („RG”, „RA”), („RG”, „R”) i inne są wywoływane kilkakrotnie.

Aby to zoptymalizować, stosujemy podejście Programowanie dynamiczne (DP).

Metoda rekurencyjna najdłuższego wspólnego podciągu

Powyższy wykres przedstawia metodę rekurencyjną. Każda funkcja rekurencyjna ma przypadek bazowy, w którym należy przerwać rekurencję lub rozpocząć zwracanie ze stosu.

Do tej implementacji użyjemy przypadku bazowego. Zatem algorytm wygląda następująco:

  • Jeżeli wszystkie elementy przed ostatnim elementem pasują, zwiększ długość o jeden i wróć.
  • Przekaż dwa wzorce do funkcji i pobierz maksymalną wartość zwracaną.
  • Jeśli jeden wzór ma zerową długość, nie mamy żadnego podciągu do porównania. W tym przypadku zwróć 0. Jest to podstawowy przypadek rekurencji.

Rzekomy Code:

def lcs:
    input: pattern_1, pattern_2, len_1, len_2
    if len_1 or len_2 is zero:
        return 0
    if pattern_1[len_1 - 1] equals pattern_2[len_2 - 1]:
        return 1 + lcs(pattern_1, pattern_2, len_1 - 1, len_2 - 1)
    else:
        return max(lcs(pattern_1, pattern_2, len_1 - 1, len_2),
                   lcs(pattern_1, pattern_2, len_1, len_2 - 1))

wdrożenie w C++

#include<iostream>
#include<bits/stdc++.h>
using namespace std;
int lcs(string pattern_1, string pattern_2, int len_1, int len_2) {
  if (len_1 == 0 || len_2 == 0)
    return 0;
  if (pattern_1[len_1 - 1] == pattern_2[len_2 - 1]) {
    return 1 + lcs(pattern_1, pattern_2, len_1 - 1, len_2 - 1);
  } else {
    return max(lcs(pattern_1, pattern_2, len_1 - 1, len_2), lcs(pattern_1, pattern_2, len_1, len_2 - 1));
  }
}
int main() {
  string pattern_1, pattern_2;
  pattern_1 = "RGBGARGA";
  pattern_2 = "BGRARG";
  cout<<"Length of LCS is: "<<lcs(pattern_1, pattern_2, pattern_1.size(), pattern_2.size())<<endl;
}

Wyjście:

Length of LCS is: 5

wdrożenie w Python

def lcs(pattern_1, pattern_2, len_1, len_2):
    if len_1 == 0 or len_2 == 0:
        return 0
    if pattern_1[len_1 - 1] == pattern_2[len_2 - 1]:
        return 1 + lcs(pattern_1, pattern_2, len_1 - 1, len_2 - 1)
    else:
        return max(lcs(pattern_1, pattern_2, len_1 - 1, len_2),
                   lcs(pattern_1, pattern_2, len_1, len_2 - 1))

pattern_1 = "RGBGARGA"
pattern_2 = "BGRARG"
print("Length of LCS is: ", lcs(pattern_1, pattern_2, len(pattern_1), len(pattern_2)))

Wyjście:

Length of LCS is:  5

Metoda programowania dynamicznego najdłuższego wspólnego podciągu (LCS)

Programowanie dynamiczne oznacza optymalizację prostej metody rekurencyjnej. Na przykład, patrząc na graf rekurencyjny lub naiwny, widzimy kilka identycznych wywołań funkcji. Metoda programowania dynamicznego rejestruje wszystkie obliczenia w tablicy i wykorzystuje je ponownie w razie potrzeby.

Użyjemy tablicy 2D o wymiarach mxn, gdzie m i n to długości wzorca 1 i wzorca 2. Dla Tablica 2D, możemy używać struktur danych List w Python lub struktury danych wektorowych/tablicowych w C++.

Rzekomy Code dla LCS wykorzystującego DP:

LCS(pattern_1, pattern_2):
    m = length of pattern_1 + 1
    n = length of pattern_2 + 1
    dp[n][m]
    for i in range 0 to n + 1:
        for j in range 0 to m + 1:
            if i or j equals to 0:
                dp[i][j] = 0
            else if pattern_1[i] == pattern_2[j]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[n][m]

Oto tabela LCS używana jako struktura danych tablicy 2D w podejściu programowania dynamicznego.

Metoda programowania dynamicznego tabeli LCS 2D

Omówmy logikę, którą tu zastosowaliśmy. Oto kroki:

Krok 1) Jeśli i lub j wynosi zero, bierzemy pusty ciąg z podanych dwóch ciągów i próbujemy znaleźć wspólne podciągi. Ponieważ jednak bierzemy pusty podciąg, długość podciągu wynosi 0.

Krok 2) Jeżeli dwa znaki są takie same, przypiszemy wartość do indeksu (i,j) poprzez zwiększenie wcześniej obliczonego LCS, który jest obecny w indeksie (i-1,j-1) (z poprzedniego wiersza).

Krok 3) Jeśli nie pasuje, bierzemy maksymalny LCS dwóch sąsiednich indeksów. W ten sposób musimy wypełnić wszystkie wartości w tablicy 2D.

Krok 4) Na koniec zwrócimy wartość ostatniej komórki tablicy 2D.

Zasadniczo wszystkie wartości w tablicy 2D zawierają długość wspólnych podciągów. Spośród nich ostatnia komórka zawiera długość najdłuższego wspólnego podciągu.

wdrożenie w C++

#include<iostream>
using namespace std;
int lcs(string pattern_1, string pattern_2) {
  int m = pattern_1.size();
  int n = pattern_2.size();
  // dp will store solutions as the iteration goes on
  int dp[n + 1][m + 1];
  for (int i = 0; i < n + 1; i++) {
    for (int j = 0; j < m + 1; j++) {
      if (i == 0 || j == 0) {
        dp[i][j] = 0;
      } else if (pattern_2[i - 1] == pattern_1[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1;
      } else {
        dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
  }
  return dp[n][m];
}
int main() {
  string pattern_1 = "RGBGARGA";
  string pattern_2 = "BGRARG";
  cout<<"Length of LCS: "<<lcs(pattern_1, pattern_2)<<endl;
}

Wyjście:

Length of LCS: 5

wdrożenie w Python

def lcs(pattern_1, pattern_2):
    m = len(pattern_1)
    n = len(pattern_2)
    # dp will store solutions as the iteration goes on
    dp = [[None] * (n + 1) for item in range(m + 1)]
    for i in range(m + 1):
        for j in range(n + 1):
            if i == 0 or j == 0:
                dp[i][j] = 0
            elif pattern_1[i - 1] == pattern_2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[m][n]

pattern_1 = "RGBGARGA"
pattern_2 = "BGRARG"
print("Length of LCS: ", lcs(pattern_1, pattern_2))

Wyjście:

Length of LCS: 5

Zatem oba ciągi mają najdłuższy wspólny podciąg o długości 5.

Krótko mówiąc, w metodzie DP po prostu obliczamy każde zadanie raz. W metodzie rekurencyjnej moglibyśmy mieć nakładanieping podproblemy.

W tym algorytmie programowania dynamicznego używamy macierzy 2D. Podane zostaną dwa ciągi znaków (zakładając, że oba mają długość n). Wtedy miejsce potrzebne w tablicy wynosi nx n. Jeśli ciągi znaków są wystarczająco duże, będziemy potrzebować zoptymalizowanej pod kątem pamięci wersji rozwiązania DP.

Uproszczona logika przyjęta w kodzie to:

  • Zadeklaruj tablicę 2D DP[m][n].
  • Wypełnij pierwszy wiersz i pierwszą kolumnę tablicy DP wartością 0.
  • Weź i i j dla iteracji.
  • Jeśli wzorzec1[i] jest równy wzorcowi2[j], wtedy zaktualizuj DP[i][j] = DP[i-1][j-1] + 1.
  • Jeżeli wzorzec1[i] nie jest równy wzorcowi2[j], wówczas DP[i][j] będzie wartością maksymalną pomiędzy DP[i-1][j] i DP[i][j-1].
  • Kontynuuj, aż i i j dotrą do m i n.
  • Ostatni element DP[m-1][n-1] będzie zawierał długość.

Tutaj adresujemy go jako DP[m-1][n-1], ponieważ indeks tablicy zaczyna się od 0.

FAQ

Procesy uczenia maszynowego wykorzystują LCS jako cechę podobieństwa w klasyfikacji tekstu, ocenie sekwencji i detektorach plagiatu kodu. Stanowi on również podstawę metryk typu BLEU i ROUGE, które oceniają wygenerowany tekst w odniesieniu do wyników referencyjnych.

Tak. Asystenci kodowania AI, tacy jak GitHub Copilot i GPT, mogą tworzyć rekurencyjne i dynamiczne wersje programowania LCS w Python, C++lub JavaMogą również na żądanie dodać funkcję zapamiętywania, wydrukować rzeczywisty podciąg lub przekonwertować kod do formy iteracyjnej.

Podciąg musi być ciągły, a podsekwencja musi jedynie zachować kolejność. W przypadku „ABCDE” „ACD” jest poprawnym podciągiem, ale nie podciągiem, natomiast „BCD” jest zarówno podciągiem, jak i podciągiem.

Wersja programowania dynamicznego działa w czasie i przestrzeni O(m·n), gdzie m i n to długości dwóch sekwencji wejściowych. Wersja rekurencyjna działa w najgorszym przypadku w czasie wykładniczym O(2^(m+n)).

Platforma LCS obsługuje narzędzia do porównywania plików, scalania plików Git, wyrównywania sekwencji DNA i białek w bioinformatyce, wykrywania plagiatów, sprawdzania pisowni i narzędzia do synchronizacji danych, które muszą zachowywać wspólną kolejność rekordów.

Standardowa tabela wymaga przestrzeni O(m·n). Optymalizacja dwuwierszowa zmniejsza przestrzeń do O(min(m, n)), gdy potrzebna jest tylko długość, choć rekonstrukcja rzeczywistego podciągu nadal wymaga całej tabeli.

Tak, czysta rekurencja działa w przypadku krótkich ciągów znaków, ale wielokrotnie przelicza te same podproblemy i staje się niepraktyczna dla ciągów powyżej 20–25 znaków. Dodanie pamięci lub tabeli DP przywraca tracwydajność tabeli.

Tak. Idea DP rozszerza się na k sekwencji, wykorzystując k-wymiarową tablicę z czasem i przestrzenią O(n^k). Ten wariant pojawia się w narzędziach do porównywania wielu plików oraz w wyrównywaniu wielu sekwencji w bioinformatyce.

Podsumuj ten post następująco: