Longest Common Subsequence
Otrzymujesz dwa ciągi znaków: text1 i text2. Podciąg ciągu znaków zachowuje niektóre jego litery w ich pierwotnej kolejności, a pozostałe pomija; zachowane litery nie muszą sąsiadować ze sobą. Zwróć długość najdłuższego ciągu znaków, który jest podciągiem obu ciągów, lub 0, jeśli oba ciągi nie mają wspólnej litery.
Funkcja
- text1string
- pierwszy ciąg znaków
- text2string
- drugi ciąg znaków
- Zwracainteger
- długość najdłuższego wspólnego podciągu
Ograniczenia
1 ≤ text1.length ≤ 10001 ≤ text2.length ≤ 1000- Oba ciągi zawierają wyłącznie małe litery alfabetu angielskiego.
Przykłady
- Wejście
- text1 = "stone"text2 = "longest"
- Wyjście
- 3
- Wyjaśnienie
- Litery o, n, e występują w tej kolejności w obu słowach, więc
onejest wspólnym podciągiem o długości 3. W słowielongestlitery s i t występują na końcu, a w słowiestonena początku, więc wspólnym podciągiem, który je zawiera, może być tylkost, który jest krótszy.
- Wejście
- text1 = "pear"text2 = "reap"
- Wyjście
- 2
- Wyjaśnienie
eawystępuje w obu słowach. W obu słowach litery p i r znajdują się po przeciwnych stronachea, więc żadna z nich nie może do niego dołączyć, a odpowiedź to 2.
- Wejście
- text1 = "cat"text2 = "dog"
- Wyjście
- 0
- Wyjaśnienie
- Te dwa słowa nie mają żadnej wspólnej litery, więc jedynym wspólnym podciągiem jest pusty podciąg o długości 0.
+19 ukrytych testów przy wysłaniu
Pytanie dodatkowe
Czy potrafisz zwrócić jedną najdłuższą wspólną podsekwencję, a nie tylko jej długość?
Podpowiedzi
Otwieraj je po kolei. Każda zdradza trochę więcej.
Spójrz na ostatnią literę każdego ciągu znaków. Co możesz powiedzieć o odpowiedzi, gdy te dwie litery są takie same, a co, gdy się różnią?
Jeśli litery są takie same, połącz je w parę, a pozostała część to ten sam problem dla obu ciągów znaków po usunięciu tej litery. Jeśli się różnią, co najmniej jedna z nich nie jest używana, więc spróbuj usunąć każdą z nich i wybierz lepszy wynik.
Te same pary prefiksów pojawiają się raz za razem. Zapisz odpowiedź dla każdej pary długości prefiksów
(i, j)w tabeli, zacznij od pustych prefiksów, których odpowiedzią jest 0, wypełniaj ją wiersz po wierszu i odczytaj odpowiedź z ostatniej komórki.
Rozwiązanie
Chciwe dopasowywanie liter nie działa. Litera może pasować do wielu miejsc w drugim ciągu, a pierwsze dopasowanie może zablokować lepsze: dopasowanie c z cab do c na końcu abc nie pozostawia miejsca na a i b, a pominięcie go pozwala znaleźć ab. Rozwiązaniem jest spostrzeżenie, że wynik dla dwóch prefiksów zależy tylko od wyników dla nieco krótszych prefiksów. Tabela zawierająca (n+1) × (m+1) liczb rozwiązuje każdą parę dokładnie raz, a ponieważ każdy wiersz odczytuje dane tylko z poprzedniego wiersza, wystarczą dwa wiersze.
Porównaj pierwsze litery za pomocą rekurencji
Poprawne, ale nie kończy się na największych testach
Intuicja
Niech lcs(i, j) oznacza wynik dla sufiksów text1[i:] i text2[j:]. Przyjrzyj się ich pierwszym literom. Jeśli są takie same, połącz je w parę: najdłuższy wspólny podciąg, który nie zawiera tej pary, może zamienić swoją pierwszą parę na tę parę, nie stając się krótszy. Zatem wynik to 1 + lcs(i+1, j+1).
Jeśli litery się różnią, nie można użyć obu, ponieważ każda z nich mogłaby być dopasowana tylko do późniejszej litery w drugim ciągu, a pary by się krzyżowały. Można więc odrzucić jedną z nich: wynik to max(lcs(i+1, j), lcs(i, j+1)). Gdy którykolwiek sufiks jest pusty, nie ma nic wspólnego i wynikiem jest 0.
To rozwiązanie jest powolne, ponieważ przy każdym niedopasowaniu rozpoczynają się dwa wywołania. Jeśli ciągi nie mają żadnej wspólnej litery, każde wywołanie napotyka niedopasowanie, dopóki jeden z ciągów się nie skończy, a liczba wywołań rośnie jak liczba sposobów przeplatania obu ciągów. Dla dwóch ciągów po 20 liter daje to około 2.8 × 10^11 wywołań; duże testy zawierają po 1000 liter. Istnieje jednak tylko (n+1) × (m+1) różnych par (i, j), więc niemal każde wywołanie powtarza wcześniejsze.
Algorytm
- Napisz
lcs(i, j)dla sufiksów zaczynających się na pozycjachiij. - Jeśli
ilubjwykracza poza koniec swojego ciągu znaków, zwróć 0. - Jeśli
text1[i] == text2[j], zwróć1 + lcs(i+1, j+1). - W przeciwnym razie zwróć
max(lcs(i+1, j), lcs(i, j+1)). - Odpowiedzią jest
lcs(0, 0).
def longestCommonSubsequence(text1, text2):
def lcs(i, j):
# The longest common subsequence of text1[i:] and text2[j:]
if i == len(text1) or j == len(text2):
return 0
if text1[i] == text2[j]:
return 1 + lcs(i + 1, j + 1)
return max(lcs(i + 1, j), lcs(i, j + 1))
return lcs(0, 0)Wypełnij tabelę prefiksów
Intuicja
Stan. Niech dp[i][j] oznacza najdłuższy wspólny podciąg pierwszych i liter text1 i pierwszych j liter text2. Praca z prefiksami pozwala oznaczać pusty ciąg indeksem 0.
Rekurencja. Porównaj ostatnie litery obu prefiksów: text1[i-1] i text2[j-1]. Jeśli są równe, połącz je w parę: dp[i][j] = dp[i-1][j-1] + 1. Jeśli nie, pomiń jedną z nich: dp[i][j] = max(dp[i-1][j], dp[i][j-1]). To samo rozumowanie co w rekurencji, tylko zaczynamy od końca. Przypadek bazowy: wiersz 0 i kolumna 0 mają wartość 0, ponieważ pusty prefiks nie ma nic wspólnego z żadnym ciągiem. Kolejność: każda komórka odczytuje komórkę powyżej, po lewej i po przekątnej w górę i w lewo, więc wypełnianie wiersz po wierszu, od lewej do prawej, zawsze zapewnia, że te komórki są już gotowe. Odpowiedzią jest dp[n][m].
Dla pear i reap wiersz dla pea to [0, 0, 1, 2, 2]. Wartość komórki dla rea wynosi 2, ponieważ a pasuje do a, więc jest równa wartości komórki dla pe i re, czyli 1, powiększonej o jeden. Ostatnia komórka, dla pear i reap, porównuje r z p. Litery te są różne, więc wybierana jest większa wartość spośród dwóch sąsiednich komórek: 2.
Tablica ma (n+1) × (m+1) komórek, a obliczenie każdej wymaga stałej liczby operacji: około 10^6 kroków dla dwóch ciągów po 1000 liter. Wersja rekurencji z memoizacją wypełnia te same komórki, ale jej wywołania rekurencyjne mogą sięgać głębokości n + m, co powoduje przepełnienie domyślnego stosu wywołań w językach takich jak Python.
Algorytm
- Utwórz tablicę
dpz(n+1) × (m+1)zer. - Dla
iod 1 donijod 1 domporównajtext1[i-1]ztext2[j-1]. - Jeśli są zgodne, ustaw
dp[i][j] = dp[i-1][j-1] + 1. - W przeciwnym razie ustaw
dp[i][j] = max(dp[i-1][j], dp[i][j-1]). - Zwróć
dp[n][m].
def longestCommonSubsequence(text1, text2):
n, m = len(text1), len(text2)
# dp[i][j]: the longest common subsequence of text1[:i] and text2[:j].
# Row 0 and column 0 stay 0: an empty prefix has nothing in common.
dp = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, m + 1):
if text1[i - 1] == text2[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]Zachowaj tylko dwa wiersze
Intuicja
Wiersz i tabeli odczytuje tylko wiersz i-1 i wcześniejsze komórki w tym samym wierszu. Gdy wiersz jest gotowy, żaden wiersz powyżej niego nie jest już odczytywany. Dlatego używaj dwóch tablic: prev dla ukończonego wiersza i cur dla wiersza, który jest wypełniany, a po każdym wierszu zamieniaj je miejscami. Rekurencja i kolejność pozostają dokładnie takie same.
Wspólny podciąg dwóch ciągów nie zależy od tego, który z nich jest pierwszy, więc możesz zamienić je miejscami i prowadzić wiersze wzdłuż krótszego ciągu. Każdy wiersz zawiera wtedy min(n, m) + 1 liczb: 1001 zamiast miliona komórek dla największych danych wejściowych, przy tej samej liczbie 10^6 kroków obliczeń.
Pierwszy element każdego wiersza odpowiada pustemu prefiksowi krótszego ciągu, więc musi pozostać równy 0. Odpowiedź to ostatni element ostatniego ukończonego wiersza.
Algorytm
- Jeśli
text2jest dłuższy niżtext1, zamień je miejscami. - Utwórz
previcur, każde zawierającem + 1zer, gdziemto krótsza długość. - Dla każdej litery
text1wypełnijcur[1..m]tą samą regułą co w tabeli, odczytującprevz poprzedniego wiersza. - Zamień
previcurmiejscami. - Zwróć
prev[m].
def longestCommonSubsequence(text1, text2):
if len(text2) > len(text1):
text1, text2 = text2, text1 # keep the rows as short as the shorter string
m = len(text2)
# prev[j]: the answer for the previous prefix of text1 and text2[:j]
prev = [0] * (m + 1)
for ch in text1:
cur = [0] * (m + 1)
for j in range(1, m + 1):
if ch == text2[j - 1]:
cur[j] = prev[j - 1] + 1
else:
cur[j] = max(prev[j], cur[j - 1])
prev = cur
return prev[m]
Pułapki i przypadki brzegowe
Rekurencja jest krótka, a większość błędów wynika z przesunięcia o jeden indeks lub dodania dopasowania w niewłaściwym miejscu.
- Mieszanie indeksów tablicy z indeksami ciągu znaków. Komórka
dp[i][j]porównujetext1[i-1]ztext2[j-1], ponieważ wiersz 0 odpowiada pustemu prefiksowi. - Przy dopasowaniu dodawanie jedynki do
max(dp[i-1][j], dp[i][j-1])zamiast dodp[i-1][j-1]. W ten sposób można użyć jednej litery dwa razy: dlaaaiawynik wyniósłby 2 zamiast 1. - Chciwe dopasowywanie za pomocą dwóch wskaźników. Dla
cabiabczostaną sparowane dwie litery c, a wynikiem będzie 1, podczas gdyabdaje 2. - Zapisywanie do wiersza, z którego nadal odczytujesz dane. Przy użyciu dwóch wierszy każda wartość z poprzedniego wiersza musi pochodzić z
prev, acur[0]musi pozostać równe 0. - Przypadkowe rozwiązanie problemu najdłuższego wspólnego podciągu. Podciąg może pomijać litery; podciąg spójny nie może.
- Zapamiętywanie wyników rekurencji dla ciągów znaków o długości 1000. Głębokość wywołań osiąga 2000, przekraczając domyślny limit Pythona wynoszący 1000.
Najczęstsze pytania4
Jaka jest złożoność czasowa problemu najdłuższego wspólnego podciągu?
Rozwiązanie z użyciem tabeli działa w czasie O(n × m), gdzie n i m to długości obu ciągów: wypełnia jedną komórkę dla każdej pary prefiksów. Pełna tabela wymaga O(n × m) pamięci, a użycie dwóch wierszy — O(min(n, m)). Zwykła rekurencja bez tabeli ma złożoność wykładniczą.
Jaka jest różnica między najdłuższym wspólnym podciągiem a najdłuższym wspólnym podłańcuchem?
Podciąg może pomijać litery, o ile zachowana jest ich kolejność, natomiast podciąg spójny to blok sąsiadujących liter. Dla stone i longest najdłuższy wspólny podciąg to one (3), ale najdłuższy wspólny podciąg spójny to on (2). W wersji z podciągiem spójnym używa się podobnej tabeli, ale przy niezgodności komórkę resetuje się do 0 zamiast kopiować wartość z sąsiedniej komórki.
Jak wypisać samą najdłuższą wspólną podsekwencję?
Wypełnij całą tablicę, a następnie cofnij się od dp[n][m]. Gdy dwie litery w bieżącej komórce są takie same, ta litera należy do odpowiedzi: zapisz ją i przejdź po przekątnej w górę i w lewo. W przeciwnym razie przejdź do sąsiedniej komórki powyżej lub po lewej, która zawiera większą wartość. Na końcu odwróć zapisane litery. Wersja z dwoma wierszami nie może tego zrobić, ponieważ odrzuciła wcześniejsze wiersze.
Jaki związek ma LCS z narzędziami diff i odległością edycyjną?
Porównanie dwóch wersji pliku znajduje najdłuższy wspólny podciąg ich wierszy; każdy wiersz poza nim jest pokazany jako dodany lub usunięty. Podobnie najmniejsza liczba wstawień i usunięć potrzebnych do przekształcenia jednego ciągu znaków w drugi to n + m - 2 × LCS. Odległość edycyjna pozwala również zastępować litery, dlatego korzysta z własnej tabeli, w której każda komórka ma trzecią możliwość.
Podobne zadania
Zadania oparte na tych samych pomysłach. Rozwiązanie dwóch lub trzech utrwala schemat.
Python
def longestCommonSubsequence(text1, text2):
# Wpisz kod tutajPrzypadek 1
Przypadek 2
Przypadek 3
Wejście
text1 = "stone" text2 = "longest"
Oczekiwane
3