Word Break
Hai una stringa s e un elenco di parole wordDict. Restituisci true se puoi suddividere s in parti in modo che ogni parte sia una parola presente in wordDict, e false altrimenti.
Le parti mantengono il loro ordine e, insieme, usano ogni lettera di s esattamente una volta. Una parola può essere usata un numero qualsiasi di volte e non devi usare tutte le parole.
Funzione
- sstring
- la stringa da suddividere in parole
- wordDictstring-array
- le parole che puoi usare, tutte le volte che vuoi
- Restituisceboolean
- vero se s può essere suddivisa in parole del dizionario, falso altrimenti
Vincoli
1 ≤ s.length ≤ 3001 ≤ wordDict.length ≤ 10001 ≤ wordDict[i].length ≤ 20se ogni parola contiene solo lettere inglesi minuscole.- Le parole in
wordDictsono tutte diverse.
Esempi
- Input
- s = "sunflowerseed"wordDict = ["sun", "flow", "flower", "seed"]
- Output
- true
- Spiegazione
- Dividila in
sun,flower,seed. Prendereflowdoposunnon porta a nulla, perché nessuna parola inizia coner, che è ciò che rimane, quindi la prima parola che va bene non è sempre quella giusta.
- Input
- s = "bananaban"wordDict = ["ban", "ana"]
- Output
- true
- Spiegazione
ban+ana+bancopre la stringa e usabandue volte, il che è consentito.
- Input
- s = "pineappletart"wordDict = ["pine", "apple", "pineapple", "tar"]
- Output
- false
- Spiegazione
- La stringa inizia con
pine+appleoppure conpineapple, e in entrambi i casi rimanetart. L’unica parola che ci sta ètar, che lascia unatisolata, quindi nessun taglio funziona.
+21 test nascosti all’invio
Per approfondire
Restituisci il minor numero di parole che un taglio valido può usare, oppure -1 se s non può essere tagliata. Cosa cambia nella tabella e il tempo di esecuzione cambia?
Suggerimenti
Aprili uno alla volta. Ognuno rivela un po’ di più.
Il primo elemento di qualsiasi taglio è una parola che inizia con
s. Una volta scelta, quale domanda rimane?La possibilità di tagliare le lettere da un certo indice fino alla fine dipende solo da quell’indice. Ci sono solo
n + 1domande di questo tipo, quindi ricorda ogni risposta, soprattutto quellefalse.Fai in modo che
canEnd[i]indichi se le primeilettere possono essere suddivise, concanEnd[0] = true. QuindicanEnd[end]è true quando qualchecanEnd[start]è true e le lettere dastartaendformano una parola. Tieni le parole in un insieme hash e prova solo segmenti non più lunghi della parola più lunga.
Soluzione
Tagliare in modo greedy non funziona in nessuna delle due direzioni: prendere prima la parola più corta fa sì che sunflowerseed venga divisa in sun + flow, mentre prendere prima quella più lunga divide carpetal in carpet e lascia al isolato. Quindi devi provare le diverse possibilità, e una stringa può essere divisa in un numero esponenziale di modi. La chiave è che la possibilità di dividere il resto della stringa dipende solo dal punto in cui inizia il resto, quindi ci sono solo n + 1 domande diverse. Qui sotto, n è la lunghezza di s, m il numero di parole e L la lunghezza della parola più lunga.
Prova ogni parola in ogni posizione
Corretto, ma non termina sui test più grandi
Intuizione
Leggi s da sinistra. Qualunque sia il primo pezzo, deve essere una parola con cui inizia s. Prova ciascuna di queste parole e, per ognuna, poni la stessa domanda sulle lettere rimanenti. Se una qualsiasi parola porta a una suddivisione completa, la risposta è true. Se nessuna ci riesce, è false. Quando non rimane nulla, hai suddiviso tutte le lettere, quindi è un successo.
Questo prova ogni possibile prima parola, poi ogni possibile seconda parola e così via, quindi non può tralasciare una suddivisione valida, e ogni true restituito corrisponde a una suddivisione reale.
È lento perché controlla più volte le stesse parti rimanenti. Prendi 299 copie di a seguite da una b, con le parole a, aa e così via fino a dieci a. Ogni modo di suddividere le a in blocchi di al massimo dieci arriva alla b e fallisce lì, e ci sono più di 10^89 modi del genere. La ricorsione deve provarli tutti prima di poter rispondere false.
Algoritmo
- Scrivi una funzione di supporto
canSplit(start)che indica se le lettere dall’indicestartfino alla fine possono essere suddivise in parole. - Se
startè uguale alla lunghezza dis, restituiscitrue. - Per ogni parola, verifica se
sla contiene a partire dall’indicestart. - Se la contiene e
canSplit(start + length of the word)ètrue, restituiscitrue. - Se nessuna parola funziona, restituisci
false. La risposta ècanSplit(0).
def wordBreak(s, wordDict):
def can_split(start):
# Can s[start:] be cut into dictionary words?
if start == len(s):
return True # nothing left to cut
for word in wordDict:
if s.startswith(word, start) and can_split(start + len(word)):
return True
return False
return can_split(0)Ricorsione con memoizzazione
Intuizione
La risposta per un resto dipende solo dal punto in cui inizia, e start assume solo n + 1 valori. Nell'esempio con le a, si raggiunge il resto che inizia all'indice 20 dopo due blocchi di dieci, dopo venti a singole e in un enorme numero di altri modi, e la risposta è false ogni volta. Memorizza la risposta per ogni punto di partenza la prima volta che la calcoli, quindi recuperala in seguito.
Uno slot della memoizzazione ha bisogno di tre stati: non ancora calcolato, true e false. Le risposte false sono quelle che contano. Un true termina subito l'intera ricerca, quindi il lavoro ripetuto dalla ricorsione semplice si trova tutto nei rami che falliscono.
Ogni punto di partenza viene calcolato una volta e prova ogni parola, confrontando fino a L lettere, quindi il tempo è O(n × m × L): al massimo 300 × 1000 × 20 = 6 × 10^6 controlli di lettere in questo caso. La memoizzazione e lo stack delle chiamate richiedono O(n) spazio e le chiamate si annidano fino a una profondità di 300.
Algoritmo
- Crea una tabella con uno slot per ogni indice, ciascuno contrassegnato come non ancora calcolato.
- In
canSplit(start), restituiscitruealla fine della stringa e restituisci la risposta memorizzata se lo slot perstartne contiene una. - Altrimenti prova ogni parola che inizia in
start, come nella ricorsione semplice, e fermati alla prima il cui resto può essere suddiviso. - Memorizza il risultato nello slot, incluso
false, e restituiscilo. - Restituisci
canSplit(0).
def wordBreak(s, wordDict):
memo = [None] * len(s) # memo[start]: answer for s[start:], None until worked out
def can_split(start):
if start == len(s):
return True
if memo[start] is not None:
return memo[start]
result = False
for word in wordDict:
if s.startswith(word, start) and can_split(start + len(word)):
result = True
break
memo[start] = result
return result
return can_split(0)Dal basso verso l'alto sui prefissi con un insieme hash
Intuizione
Inverti il ragionamento e lavora sui prefissi. Sia canEnd[i] a indicare se le prime i lettere possono essere suddivise in parole. Il prefisso vuoto non richiede parole, quindi canEnd[0] è true. Le prime end lettere possono essere suddivise esattamente quando l’ultima parte, le lettere da start a end, è una parola e le lettere che la precedono possono essere suddivise, cioè canEnd[start] è true. Riempi la tabella da sinistra a destra e ogni canEnd[start] di cui hai bisogno è già noto.
Invece di confrontare tutte le m parole a ogni posizione, inserisci le parole in un insieme hash e cerca le possibili parti finali. Nessuna parola è più lunga di L, quindi solo le L parti che terminano in end possono corrispondere. Con sunflowerseed, canEnd diventa true a 0, a 3 (sun), a 7 (flow), a 9 (flower) e a 13 (seed dopo la posizione 9), quindi la risposta è true. La posizione 7 non porta da nessuna parte, perché nessuna parola inizia con er, e alla tabella non importa.
Ci sono n posizioni, ognuna cerca al massimo L parti, e costruire e calcolare l’hash di una parte richiede fino a L passaggi. Si tratta di O(n × L²), al massimo 300 × 20 × 20 = 1.2 × 10^5 passaggi sulle lettere, a prescindere da quanto sia grande il dizionario. La costruzione dell’insieme legge ogni parola una volta, O(m × L), quindi il totale è O(m × L + n × L²). L’insieme contiene le parole, O(m × L) lettere, e la tabella contiene n + 1 flag. Non c’è ricorsione.
Algoritmo
- Inserisci ogni parola in un insieme hash e annota la lunghezza
Ldella parola più lunga. - Crea
canEndconn + 1elementi, tuttifalse, e impostacanEnd[0]sutrue. - Per ogni
endda 1 an, prova ognilengthda 1 amin(L, end). - Se
canEnd[end-length]ètruee il pezzo di quella lunghezza che termina inendè nell'insieme, impostacanEnd[end]sutruee interrompi i tentativi con le lunghezze. - Restituisci
canEnd[n].
def wordBreak(s, wordDict):
words = set(wordDict)
longest = max(len(word) for word in wordDict)
n = len(s)
# can_end[i]: the first i letters split into dictionary words
can_end = [False] * (n + 1)
can_end[0] = True # the empty prefix needs no words
for end in range(1, n + 1):
# The last word is s[end-length:end], and no word is longer than longest.
for length in range(1, min(longest, end) + 1):
if can_end[end - length] and s[end - length:end] in words:
can_end[end] = True
break
return can_end[n]
Trappole e casi limite
La maggior parte delle risposte sbagliate deriva dal decidere troppo presto di fare un unico taglio o da una ricerca che non tiene mai conto dei propri insuccessi.
- Tagliare in modo greedy. Prendere prima la parola più lunga divide
carpetalincarpete lasciaal, anche secar+petalfunziona. Prendere prima la parola più corta non funziona consunflowerseed. - Controllare solo che ogni lettera di
scompaia in una parola. Con le paroleaaaaeaa, ogni pezzo ha una lunghezza pari, quindiaaaaaaa, sette lettere, non può essere suddivisa. - Memorizzare nella memo solo le risposte
true. Una rispostatruetermina comunque la ricerca. Il lavoro ripetuto si trova nei ramifalse, quindi una memo senza di essi continua ad avere una complessità esponenziale. - Creare una tabella con una voce in meno.
canEnd[i]riguarda le primeilettere, e sia 0 siansono valori validi, quindi servonon + 1voci. - Confrontare oltre la fine di
squando una parola è più lunga della parte rimanente, ad esempio la parolaabcrispetto aab. Controlla le lunghezze prima di confrontare le lettere. - In Lua e R, le posizioni delle stringhe iniziano da 1: un pezzo di lunghezza
kche termina alla letteraeinizia alla letterae-k+1.
Domande frequenti4
Qual è la complessità temporale di Word Break?
La tabella bottom-up con un insieme hash richiede un tempo O(m × L + n × L²), dove n è la lunghezza di s, m il numero di parole e L la parola più lunga. La creazione dell’insieme legge ogni parola una volta e, per ciascuna delle n posizioni, cerca al massimo L parti composte da un massimo di L lettere. Se invece confronti ogni parola in ogni posizione, il tempo è O(n × m × L). La ricorsione semplice senza memoizzazione è esponenziale.
Perché un approccio greedy non funziona per Word Break?
Una regola greedy sceglie una parola e non ci ripensa mai. La scelta della parola più lunga per prima divide carpetal in carpet e al, mentre car + petal funziona. La scelta della parola più corta per prima divide sunflowerseed in sun + flow e si blocca su erseed. La programmazione dinamica tiene traccia di ogni posizione raggiungibile con qualche suddivisione, così non perde mai quella giusta.
Word Break è un problema di programmazione dinamica o un problema di grafi?
Entrambe le rappresentazioni funzionano. Come programmazione dinamica, canEnd[i] risponde alla domanda se le prime i lettere possono essere suddivise, partendo da prefissi più brevi. Come grafo, ogni indice è un nodo con un arco da i a j quando le lettere da i a j formano una parola, e ci si chiede se il nodo n è raggiungibile dal nodo 0. Una ricerca in ampiezza con un insieme di nodi visitati svolge lo stesso compito della tabella.
Come fai a elencare ogni frase invece di restituire true o false?
Usa il backtracking: a ogni indice prova ogni parola compatibile e richiama ricorsivamente la funzione sul resto, costruendo la frase man mano. Ricorda l’elenco delle frasi per ogni indice, così ogni parte restante viene risolta una sola volta. Esegui prima la tabella dei valori vero o falso, in modo che una stringa che non può essere suddivisa salti la ricerca. Il numero di frasi può crescere in modo esponenziale, quindi la dimensione dell’output determina il tempo di esecuzione.
Problemi simili
Problemi che usano le stesse idee. Risolverne due o tre è ciò che fissa uno schema.
Python
def wordBreak(s, wordDict):
# Scrivi il codice quiCaso 1
Caso 2
Caso 3
Input
s = "sunflowerseed" wordDict = ["sun", "flow", "flower", "seed"]
Atteso
true