Longest Common Substring
Ricevi due stringhe, s1 e s2. Una sottostringa è una sequenza ininterrotta di lettere presa da una stringa, come ork da clockwork. Trova la sottostringa più lunga che s1 e s2 hanno in comune e restituiscine la lunghezza. Se nessuna lettera compare in entrambe le stringhe, la risposta è 0.
Funzione
- s1string
- la prima stringa
- s2string
- la seconda stringa
- Restituisceinteger
- la lunghezza del blocco di lettere più lungo presente in entrambe le stringhe
Vincoli
1 ≤ s1.length ≤ 10001 ≤ s2.length ≤ 1000s1es2sono composte da lettere minuscole dell'alfabeto inglese.
Esempi
- Input
- s1 = "clockwork"s2 = "workshop"
- Output
- 4
- Spiegazione
workconcludeclockworke iniziaworkshop, quindi è una sottostringa comune di lunghezza 4, e non compare in entrambi alcun blocco più lungo.
- Input
- s1 = "hammer"s2 = "harmed"
- Output
- 2
- Spiegazione
haemecompaiono in entrambe le parole, ma non c’è nessuna sequenza di tre lettere. Entrambe le parole contengono h, a, m, e in quest’ordine, maharmedha una r tra la a e la m.
- Input
- s1 = "fig"s2 = "plum"
- Output
- 0
- Spiegazione
- Le due parole non hanno lettere in comune, quindi la sottostringa comune più lunga è vuota e la sua lunghezza è 0.
Per approfondire
Riesci a verificare se esiste una sottostringa comune di una determinata lunghezza L in circa O(n + m) tempo con un hash rolling, e poi eseguire una ricerca binaria su L?
Suggerimenti
Aprili uno alla volta. Ognuno rivela un po’ di più.
Confrontare ogni coppia di posizioni iniziali ripete il lavoro: il blocco che inizia in
iejcontiene il blocco che inizia ini+1ej+1. Prova a riutilizzare una risposta per l’altra.Poni una domanda più circoscritta per ogni coppia di posizioni: quanto è lungo il blocco comune che termina esattamente in
s1[i]es2[j]? Se le due lettere sono diverse, è 0. Se corrispondono, è uno in più rispetto alla risposta per le due lettere precedenti.Riempi una tabella
run[i][j]con quelle lunghezze, usando la riga 0 e la colonna 0 come zeri per i prefissi vuoti. La risposta è il valore più grande in qualsiasi punto della tabella, non l’ultima cella. Ogni cella legge solo la cella in diagonale in alto a sinistra, quindi basta scorrere una riga da destra a sinistra.
Soluzione
Estendere una corrispondenza da ogni coppia di posizioni iniziali rilegge le stesse lettere. Guarda invece dove un blocco termina: il blocco comune che termina in s1[i] e s2[j] è più lungo di una lettera rispetto a quello che termina nelle due lettere che li precedono, oppure è vuoto se le lettere sono diverse. Una tabella di queste lunghezze fornisce la risposta in O(n × m) tempo.
Estendi da ogni coppia di posizioni iniziali
Corretto, ma non termina sui test più grandi
Intuizione
Ogni sottostringa comune inizia in una certa posizione i di s1 e in una certa posizione j di s2. Per ogni coppia, scorri in avanti finché le lettere corrispondono e conta i passaggi; il conteggio più alto è la risposta.
Il testo ripetitivo rende lunghi gli scorrimenti. Quando entrambe le stringhe sono 1000 copie di a, ogni coppia scorre fino alla fine del suffisso più corto: circa 3.3 × 10^8 confronti tra lettere. Lo scorrimento da (i, j) ripete per intero lo scorrimento da (i+1, j+1), una lettera più avanti.
Algoritmo
- Imposta
best = 0. - Per ogni
iins1e ognijins2, impostak = 0. - Mentre sia
i+kchej+ksono all'interno delle rispettive stringhe es1[i+k] == s2[j+k], aggiungi 1 ak. - Conserva il valore più grande di
kinbeste restituiscilo.
def longestCommonSubstring(s1, s2):
n, m = len(s1), len(s2)
best = 0
for i in range(n):
for j in range(m):
# Walk forward while the two strings agree
k = 0
while i + k < n and j + k < m and s1[i + k] == s2[j + k]:
k += 1
if k > best:
best = k
return bestTabella delle lunghezze dei blocchi che terminano a ogni coppia
Intuizione
Sia run[i][j] la lunghezza del blocco comune più lungo che termina esattamente in s1[i-1] e s2[j-1]; la riga 0 e la colonna 0 contengono 0. Un blocco di questo tipo deve contenere entrambe le lettere. Se sono diverse, non esiste alcun blocco e la cella vale 0. Se corrispondono, il blocco è questa coppia più il blocco che termina una lettera prima in entrambe le stringhe: run[i-1][j-1] + 1. Ogni sottostringa comune termina da qualche parte, quindi la risposta è la cella più grande, ovunque si trovi.
Per hammer e harmed, le h danno 1 e le a estendono il blocco a 2, poi la r interrompe il blocco. La seconda m di hammer incontra la m di harmed dopo una mancata corrispondenza tra m e r, quindi riparte da 1, e le e lo portano a 2. Nessuna cella arriva a 3.
Un confronto per cella: circa un milione per due stringhe di 1000 lettere, qualunque sia il testo.
Algoritmo
- Inizializza
runconn+1righe em+1colonne, tutte a 0, e impostabest = 0. - Esegui un ciclo su
ida 1 ane, al suo interno, sujda 1 am, confrontandos1[i-1]cons2[j-1]. - In caso di corrispondenza, imposta
run[i][j] = run[i-1][j-1] + 1e aggiornabest. - In caso di mancata corrispondenza, lascia la cella a 0.
- Restituisci
best.
def longestCommonSubstring(s1, s2):
n, m = len(s1), len(s2)
# run[i][j]: length of the longest block that ends at s1[i-1] and at s2[j-1]
run = [[0] * (m + 1) for _ in range(n + 1)]
best = 0
for i in range(1, n + 1):
for j in range(1, m + 1):
if s1[i - 1] == s2[j - 1]:
run[i][j] = run[i - 1][j - 1] + 1
if run[i][j] > best:
best = run[i][j]
# a mismatch leaves run[i][j] at 0
return bestUna riga, aggiornata da destra a sinistra
Intuizione
Ogni cella legge solo la cella in diagonale in alto a sinistra. Quindi un unico array run di m+1 numeri può contenere la tabella se scorri j da destra a sinistra: quando scrivi run[j], la posizione run[j-1] contiene ancora il valore della lettera precedente di s1, che è la cella diagonale.
Ora una mancata corrispondenza deve scrivere esplicitamente 0, perché la posizione contiene ancora un numero della riga precedente. Il tempo rimane O(n × m) e la memoria scende da circa 10^6 numeri a 1001.
Algoritmo
- Crea un array
rundim+1zeri e impostabest = 0. - Per ogni lettera
chdis1, visitajdamfino a 1. - Se
ch == s2[j-1], impostarun[j] = run[j-1] + 1e aggiornabest. - Altrimenti imposta
run[j] = 0. - Restituisci
best.
def longestCommonSubstring(s1, s2):
m = len(s2)
# run[j]: length of the common block that ends at the current letter of s1 and at s2[j-1]
run = [0] * (m + 1)
best = 0
for ch in s1:
# Right to left, so run[j-1] still holds the value from the previous letter of s1
for j in range(m, 0, -1):
if ch == s2[j - 1]:
run[j] = run[j - 1] + 1
if run[j] > best:
best = run[j]
else:
run[j] = 0
return best
Trappole e casi limite
- Restituire l'ultima cella.
run[n][m]copre solo i blocchi che terminano con entrambe le ultime lettere. Perabceabdè 0, mentreabdà 2. - Copiare il vicino più grande in caso di mancata corrispondenza. Questa è la regola della sottosequenza e permette a un blocco di saltare delle lettere: con
hammereharmedrestituisce 4 invece di 2. - Scorrere la singola riga da sinistra a destra. Il valore diagonale viene sovrascritto prima di leggerlo, quindi
aconfrontato conaadà 2. - Saltare il ripristino nella singola riga. Una lunghezza della lettera precedente di
s1sopravvive e in seguito estende un blocco che non esiste. - Scorrere la forza bruta senza controllare i limiti. Con
zzzzconfrontato conzz, legge oltre la fine della stringa più corta.
Domande frequenti3
Qual è la differenza tra la sottostringa comune più lunga e la sottosequenza comune più lunga?
Una sottostringa è una sequenza ininterrotta di lettere; una sottosequenza mantiene l’ordine, ma può lasciare degli spazi. Per hammer e harmed, la sottostringa comune più lunga ha lunghezza 2 e la sottosequenza comune più lunga ha lunghezza 4 (hame). Nelle tabelle, una mancata corrispondenza reimposta a 0 la cella della sottostringa, ma fa sì che la cella della sottosequenza copi il valore del vicino più grande.
Qual è la complessità temporale della sottostringa comune più lunga?
La soluzione di programmazione dinamica richiede un tempo O(n × m) e O(m) di memoria con una sola riga. Estendere da ogni coppia di posizioni iniziali richiede O(n × m × min(n, m)) nel caso peggiore. Un automa dei suffissi raggiunge circa O(n + m), al costo di un programma molto più lungo.
Come si restituisce direttamente la sottostringa comune più lunga?
Ogni volta che una cella supera best, registra anche la posizione i in s1 in cui termina quel blocco. Alla fine, la sottostringa è composta dalle lettere di s1 che terminano lì, in numero pari a best. Un confronto strettamente maggiore mantiene il primo blocco più lungo trovato in s1.
Problemi simili
Problemi che usano le stesse idee. Risolverne due o tre è ciò che fissa uno schema.
def longestCommonSubstring(s1, s2):
# Scrivi il codice quiTest #1
Test #2
Test #3
Argomenti
s1 = "clockwork" s2 = "workshop"
Output atteso
4