Longest Common Subsequence
Du erhältst zwei Zeichenfolgen, text1 und text2. Eine Teilsequenz einer Zeichenfolge behält einige ihrer Buchstaben in ihrer ursprünglichen Reihenfolge bei und lässt die übrigen weg; die beibehaltenen Buchstaben müssen nicht nebeneinanderstehen. Gib die Länge der längsten Zeichenfolge zurück, die eine Teilsequenz beider Zeichenfolgen ist, oder 0, wenn die beiden Zeichenfolgen keinen Buchstaben gemeinsam haben.
Funktion
- text1string
- die erste Zeichenkette
- text2string
- die zweite Zeichenkette
- Gibt zurückinteger
- die Länge der längsten gemeinsamen Teilsequenz
Einschränkungen
1 ≤ text1.length ≤ 10001 ≤ text2.length ≤ 1000- Beide Zeichenfolgen enthalten ausschließlich englische Kleinbuchstaben.
Beispiele
- Eingabe
- text1 = "stone"text2 = "longest"
- Ausgabe
- 3
- Erklärung
- o, n, e erscheinen in beiden Wörtern in dieser Reihenfolge, daher ist
oneeine gemeinsame Teilfolge der Länge 3. Inlongestkommen die Buchstaben s und t zuletzt, während sie instonezuerst kommen, sodass eine gemeinsame Teilfolge, die sie verwendet, nurstsein kann, was kürzer ist.
- Eingabe
- text1 = "pear"text2 = "reap"
- Ausgabe
- 2
- Erklärung
eakommt in beiden Wörtern vor. Das p und das r stehen in den beiden Wörtern auf gegenüberliegenden Seiten vonea, sodass sich keines davon damit verbinden kann, und die Antwort ist 2.
- Eingabe
- text1 = "cat"text2 = "dog"
- Ausgabe
- 0
- Erklärung
- Die beiden Wörter haben keinen gemeinsamen Buchstaben, daher ist die einzige gemeinsame Teilsequenz die leere Teilsequenz mit der Länge 0.
+19 versteckte Tests beim Einreichen
Weiterführende Frage
Kannst du eine längste gemeinsame Teilsequenz selbst zurückgeben, nicht nur ihre Länge?
Tipps
Öffne sie nacheinander. Jeder verrät ein bisschen mehr.
Sieh dir den letzten Buchstaben jeder Zeichenkette an. Was kannst du über die Antwort sagen, wenn die beiden Buchstaben gleich sind, und was, wenn sie unterschiedlich sind?
Wenn die Buchstaben übereinstimmen, ordne sie einander zu; der Rest ist dasselbe Problem für beide Zeichenfolgen, nachdem dieser Buchstabe entfernt wurde. Wenn sie sich unterscheiden, wird mindestens einer der beiden nicht verwendet. Versuche also, jeweils einen davon zu entfernen, und behalte das bessere Ergebnis.
Dieselben Präfixlängenpaare tauchen immer wieder auf. Speichere die Antwort für jedes Paar von Präfixlängen
(i, j)in einer Tabelle, beginne mit den leeren Präfixen, deren Antwort 0 ist, fülle sie Zeile für Zeile aus und lies die Antwort aus der letzten Zelle ab.
Lösung
Gieriges Zuordnen von Buchstaben funktioniert nicht. Ein Buchstabe kann an vielen Stellen im anderen String passen, und der erste Treffer kann bessere Möglichkeiten blockieren: Wenn man das c von cab mit dem c am Ende von abc paart, bleibt nichts für a und b übrig, während man durch Überspringen ab findet. Der entscheidende Gedanke ist, dass die Antwort für zwei Präfixe nur von den Antworten für etwas kürzere Präfixe abhängt. Eine Tabelle mit (n+1) × (m+1) Zahlen löst jedes Paar genau einmal, und da jede Zeile nur die Zeile darüber liest, reichen zwei Zeilen aus.
Erste Buchstaben mithilfe von Rekursion vergleichen
Korrekt, wird aber bei den größten Tests nicht fertig
Idee
Sei lcs(i, j) die Antwort für die Suffixe text1[i:] und text2[j:]. Betrachte ihre ersten Buchstaben. Sind sie gleich, ordne sie einander zu: Eine längste gemeinsame Teilfolge, die dieses Paar nicht verwendet, kann ihr erstes Paar durch dieses ersetzen, ohne kürzer zu werden. Die Antwort lautet also 1 + lcs(i+1, j+1).
Unterscheiden sich die Buchstaben, können sie nicht beide verwendet werden, da jeder nur mit einem späteren Buchstaben der jeweils anderen Zeichenkette abgeglichen werden könnte und sich die Paare dann kreuzen würden. Also kann einer von ihnen ausgelassen werden: Die Antwort lautet max(lcs(i+1, j), lcs(i, j+1)). Wenn eines der Suffixe leer ist, gibt es nichts Gemeinsames und die Antwort ist 0.
Das Verfahren ist langsam, weil bei jeder Nichtübereinstimmung zwei Aufrufe gestartet werden. Wenn die Zeichenketten keinen gemeinsamen Buchstaben haben, liegt bei jedem Aufruf eine Nichtübereinstimmung vor, bis eine Zeichenkette aufgebraucht ist, und die Anzahl der Aufrufe wächst wie die Anzahl der Möglichkeiten, die beiden Zeichenketten miteinander zu verschachteln. Bei zwei Zeichenketten mit jeweils 20 Buchstaben sind das etwa 2.8 × 10^11 Aufrufe; die großen Tests enthalten jeweils 1000 Buchstaben. Es gibt jedoch nur (n+1) × (m+1) verschiedene Paare (i, j), daher wiederholen fast alle Aufrufe einen früheren.
Algorithmus
- Schreibe
lcs(i, j)für die Suffixe, die beiiundjbeginnen. - Wenn
ioderjhinter dem Ende seiner Zeichenfolge liegt, gib 0 zurück. - Wenn
text1[i] == text2[j], gib1 + lcs(i+1, j+1)zurück. - Andernfalls gib
max(lcs(i+1, j), lcs(i, j+1))zurück. - Die Antwort ist
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)Fülle eine Tabelle mit Präfixen aus
Idee
Zustand. Sei dp[i][j] die längste gemeinsame Teilsequenz der ersten i Buchstaben von text1 und der ersten j Buchstaben von text2. Bei Präfixen kann der Index 0 für eine leere Zeichenfolge stehen.
Rekurrenz. Vergleiche die letzten Buchstaben der beiden Präfixe, text1[i-1] und text2[j-1]. Sind sie gleich, ordne sie einander zu: dp[i][j] = dp[i-1][j-1] + 1. Andernfalls lass einen von ihnen weg: dp[i][j] = max(dp[i-1][j], dp[i][j-1]). Das ist dieselbe Überlegung wie bei der Rekursion, nur vom Ende aus gelesen. Basisfall: Zeile 0 und Spalte 0 enthalten den Wert 0, denn ein leeres Präfix hat nichts mit irgendeinem anderen gemeinsam. Reihenfolge: Jede Zelle liest die Zelle darüber, die Zelle links davon und die diagonal links oben, daher sind diese beim Ausfüllen Zeile für Zeile von links nach rechts immer bereits ausgefüllt. Die Antwort ist dp[n][m].
Für pear und reap lautet die Zeile für pea [0, 0, 1, 2, 2]. Der Wert ihrer Zelle für rea ist 2, weil a mit a übereinstimmt. Daher ist er gleich dem Wert der Zelle für pe und re, 1, plus eins. In der letzten Zelle werden bei pear und reap r und p verglichen, die unterschiedlich sind; deshalb wird der größere Wert der beiden benachbarten Zellen übernommen, also 2.
Die Tabelle hat (n+1) × (m+1) Zellen, und für jede ist ein konstanter Aufwand erforderlich: etwa 10^6 Schritte für zwei Zeichenfolgen mit je 1000 Buchstaben. Eine memoisierten Version der Rekursion füllt dieselben Zellen aus, rekursiert aber bis zu einer Tiefe von n + m Aufrufen, wodurch der standardmäßige Aufrufstapel in Sprachen wie Python überläuft.
Algorithmus
- Erstelle eine Tabelle
dpmit(n+1) × (m+1)Nullen. - Vergleiche für
ivon 1 bisnundjvon 1 bismtext1[i-1]mittext2[j-1]. - Bei einer Übereinstimmung setze
dp[i][j] = dp[i-1][j-1] + 1. - Andernfalls setze
dp[i][j] = max(dp[i-1][j], dp[i][j-1]). - Gib
dp[n][m]zurück.
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]Behalte nur zwei Zeilen
Idee
Zeile i der Tabelle liest nur Zeile i-1 und ihre eigenen früheren Zellen. Sobald eine Zeile fertig ist, wird keine Zeile darüber jemals wieder gelesen. Verwende also zwei Arrays: prev für die fertige Zeile und cur für die gerade ausgefüllte Zeile, und vertausche sie nach jeder Zeile. Die Rekurrenz und die Reihenfolge bleiben genau gleich.
Eine gemeinsame Teilsequenz zweier Zeichenketten hängt nicht davon ab, welche Zeichenkette zuerst kommt. Daher kannst du sie vertauschen und die Zeilen entlang der kürzeren Zeichenkette laufen lassen. Jede Zeile enthält dann min(n, m) + 1 Zahlen: 1001 statt einer Million Zellen für die größten Eingaben, bei derselben Anzahl von 10^6 Arbeitsschritten.
Der erste Eintrag jeder Zeile steht für ein leeres Präfix der kürzeren Zeichenkette und muss daher 0 bleiben. Die Antwort ist der letzte Eintrag der letzten fertigen Zeile.
Algorithmus
- Wenn
text2länger alstext1ist, tausche sie. - Erstelle
prevundcur, jeweils mitm + 1Nullen, wobeimdie kürzere Länge ist. - Fülle für jeden Buchstaben von
text1cur[1..m]nach derselben Regel wie in der Tabelle aus und verwende dabeiprevfür die Zeile darüber. - Tausche
prevundcur. - Gib
prev[m]zurück.
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]
Stolperfallen und Grenzfälle
Die Rekurrenz ist kurz, und die meisten Fehler entstehen durch einen Off-by-one-Fehler oder dadurch, dass ein Treffer an der falschen Stelle hinzugefügt wird.
- Tabellenindizes mit Zeichenkettenindizes verwechseln. Die Zelle
dp[i][j]vergleichttext1[i-1]mittext2[j-1], da Zeile 0 dem leeren Präfix entspricht. - Bei einer Übereinstimmung 1 zu
max(dp[i-1][j], dp[i][j-1])statt zudp[i-1][j-1]addieren. Dadurch kann derselbe Buchstabe zweimal verwendet werden:aagegenüberawürde 2 statt 1 ergeben. - Gierig mit zwei Zeigern abgleichen.
cabgegenüberabcordnet die beiden c-Buchstaben einander zu und ergibt 1, währendab2 ergibt. - In die Zeile schreiben, aus der gerade gelesen wird. Bei zwei Zeilen muss jeder Wert aus der Zeile darüber aus
prevstammen, undcur[0]muss 0 bleiben. - Aus Versehen das längste gemeinsame Teilwort lösen. Eine Teilsequenz darf Buchstaben überspringen; ein Teilwort nicht.
- Rekursion für 1000 Zeichen lange Zeichenketten mit Memoisierung verwenden. Die Aufruftiefe erreicht 2000 und überschreitet damit Pythons Standardlimit von 1000.
Häufige Fragen4
Wie hoch ist die Zeitkomplexität der längsten gemeinsamen Teilsequenz?
Die Tabellenlösung benötigt O(n × m) Zeit, wobei n und m die beiden Längen sind: Sie füllt eine Zelle pro Paar von Präfixen. Für die vollständige Tabelle benötigt sie O(n × m) Speicher oder mit zwei Zeilen O(min(n, m)). Einfache Rekursion ohne Tabelle hat eine exponentielle Laufzeit.
Was ist der Unterschied zwischen der längsten gemeinsamen Teilsequenz und der längsten gemeinsamen Teilzeichenfolge?
Eine Teilsequenz kann Buchstaben überspringen, solange die Reihenfolge beibehalten wird, während eine Teilzeichenfolge ein Block benachbarter Buchstaben ist. Für stone und longest ist die längste gemeinsame Teilsequenz one (3), aber die längste gemeinsame Teilzeichenfolge ist on (2). Die Variante mit Teilzeichenfolgen verwendet eine ähnliche Tabelle, aber bei einer Nichtübereinstimmung wird die Zelle auf 0 zurückgesetzt, anstatt den Wert eines Nachbarn zu übernehmen.
Wie gibst du die längste gemeinsame Teilsequenz selbst aus?
Fülle die gesamte Tabelle aus und gehe dann von dp[n][m] aus rückwärts. Wenn die beiden Buchstaben in der aktuellen Zelle übereinstimmen, gehört dieser Buchstabe zur Antwort: Halte ihn fest und gehe diagonal nach oben links. Andernfalls gehe zu dem Nachbarn darüber oder links davon, der den größeren Wert enthält. Kehre die Reihenfolge der festgehaltenen Buchstaben am Ende um. Mit der Variante mit zwei Zeilen geht das nicht, weil sie die früheren Zeilen verworfen hat.
Wie hängt LCS mit Diff-Tools und der Editierdistanz zusammen?
Ein Diff zwischen zwei Versionen einer Datei ermittelt die längste gemeinsame Teilsequenz ihrer Zeilen; jede Zeile außerhalb davon wird als hinzugefügt oder entfernt angezeigt. Genauso ist die kleinste Anzahl von Einfügungen und Löschungen, mit der sich eine Zeichenfolge in die andere umwandeln lässt, n + m - 2 × LCS. Die Editierdistanz ermöglicht auch das Ersetzen eines Buchstabens, daher verwendet sie eine eigene Tabelle mit einer dritten Auswahl pro Zelle.
Ähnliche Aufgaben
Aufgaben mit denselben Ideen. Wer zwei oder drei davon löst, behält das Muster.
Python
def longestCommonSubsequence(text1, text2):
# Schreibe hier den CodeFall 1
Fall 2
Fall 3
Eingabe
text1 = "stone" text2 = "longest"
Erwartet
3