Word Break
Você recebe uma string s e uma lista de palavras wordDict. Retorne true se puder dividir s em partes de modo que cada parte seja uma palavra de wordDict, e false caso contrário.
As partes mantêm a ordem e, juntas, usam cada letra de s exatamente uma vez. Uma palavra pode ser usada quantas vezes quiser, e você não precisa usar todas as palavras.
Função
- sstring
- a string a ser dividida em palavras
- wordDictstring-array
- as palavras que você pode usar, cada uma quantas vezes quiser
- Retornaboolean
- true se s puder ser dividido em palavras do dicionário, false caso contrário
Restrições
1 ≤ s.length ≤ 3001 ≤ wordDict.length ≤ 10001 ≤ wordDict[i].length ≤ 20se cada palavra contém apenas letras minúsculas do inglês.- As palavras em
wordDictsão todas diferentes.
Exemplos
- Entrada
- s = "sunflowerseed"wordDict = ["sun", "flow", "flower", "seed"]
- Saída
- true
- Explicação
- Divida como
sun,flower,seed. Pegarflowdepois desunnão leva a lugar nenhum, pois nenhuma palavra começa com oerque sobra, então a primeira palavra que se encaixa nem sempre é a certa.
- Entrada
- s = "bananaban"wordDict = ["ban", "ana"]
- Saída
- true
- Explicação
ban+ana+bancobre a string e usabanduas vezes, o que é permitido.
- Entrada
- s = "pineappletart"wordDict = ["pine", "apple", "pineapple", "tar"]
- Saída
- false
- Explicação
- A string começa com
pine+appleou compineapple, e ambos deixamtart. A única palavra que se encaixa ali étar, que deixa umtisolado, então nenhum corte funciona.
+21 testes ocultos ao enviar
Para ir além
Retorne o menor número de palavras que uma segmentação válida pode usar, ou -1 se s não puder ser segmentada. O que muda na tabela, e o tempo de execução muda?
Dicas
Abra uma de cada vez. Cada uma revela um pouco mais.
A primeira parte de qualquer corte é uma palavra que começa com
s. Depois de escolhê-la, que pergunta resta?Se as letras de algum índice até o final podem ser removidas depende apenas desse índice. Há apenas
n + 1perguntas desse tipo, então memorize cada resposta, especialmente asfalse.Considere que
canEnd[i]indica se as primeirasiletras podem ser separadas, comcanEnd[0] = true. Então,canEnd[end]é verdadeiro quando algumcanEnd[start]é verdadeiro e as letras destartaendformam uma palavra. Mantenha as palavras em um conjunto hash e tente apenas trechos com comprimento não maior que o da palavra mais longa.
Solução
Cortar de forma gulosa falha nos dois sentidos: escolher primeiro a palavra mais curta divide sunflowerseed em sun + flow, e escolher primeiro a mais longa divide carpetal em carpet e deixa al sem correspondência. Então você precisa testar as opções, e uma string pode ser dividida de exponencialmente muitas maneiras. O que resolve o problema é que a possibilidade de dividir o restante da string depende apenas de onde esse restante começa, então há apenas n + 1 perguntas diferentes. Abaixo, n é o comprimento de s, m é o número de palavras e L é o comprimento da palavra mais longa.
Tente cada palavra em cada posição
Correta, mas não termina nos maiores testes
Intuição
Leia s da esquerda. Seja qual for a primeira parte, ela precisa ser uma palavra com que s comece. Experimente cada palavra desse tipo e, para cada uma, faça a mesma pergunta sobre as letras restantes. Se alguma palavra levar a um corte completo, a resposta é true. Se nenhuma levar, é false. Quando não restar nada, você terá cortado todas as letras, então isso conta como sucesso.
Isso tenta todas as primeiras palavras possíveis, depois todas as segundas palavras possíveis e assim por diante, então não pode deixar passar um corte válido, e todo true retornado vem acompanhado de um corte real.
É lento porque verifica os mesmos trechos restantes repetidamente. Considere 299 cópias de a seguidas por um b, com as palavras a, aa e assim por diante até dez as. Toda maneira de dividir os a's em blocos de no máximo dez chega ao b e falha ali, e há mais de 10^89 maneiras desse tipo. A recursão precisa tentar todas antes de poder responder false.
Algoritmo
- Escreva uma função auxiliar
canSplit(start)que indique se as letras do índicestartaté o final podem ser divididas em palavras. - Se
startfor igual ao comprimento des, retornetrue. - Para cada palavra, verifique se
sa contém começando no índicestart. - Se contiver e
canSplit(start + length of the word)fortrue, retornetrue. - Se nenhuma palavra funcionar, retorne
false. A resposta é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)Recursão com memoização
Intuição
A resposta para um resto depende apenas de onde ele começa, e start assume apenas n + 1 valores. No exemplo com a's, o resto que começa no índice 20 é alcançado após dois blocos de dez, após vinte as individuais e de inúmeras outras maneiras, e a resposta é false todas as vezes. Armazene a resposta para cada posição inicial na primeira vez que a calcular e consulte-a depois.
Uma posição da memoização precisa de três estados: ainda não calculada, true e false. As respostas false são as que importam. Um true encerra toda a busca imediatamente, então o trabalho repetido pela recursão simples ocorre todo nos ramos que falham.
Cada posição inicial é calculada uma vez e testa todas as palavras, comparando até L letras, então o tempo é O(n × m × L): no máximo 300 × 1000 × 20 = 6 × 10^6 verificações de letras aqui. A memoização e a pilha de chamadas ocupam O(n) espaço, e as chamadas se aninham até 300 níveis.
Algoritmo
- Crie uma tabela de memorização com uma posição para cada índice, cada uma marcada como ainda não calculada.
- Em
canSplit(start), retornetrueno fim da string e retorne a resposta armazenada se a posição destartcontiver uma. - Caso contrário, tente cada palavra que começa em
start, como na recursão simples, e pare na primeira cuja parte restante puder ser dividida. - Armazene o resultado na posição, incluindo
false, e retorne-o. - Retorne
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)De baixo para cima sobre prefixos com um conjunto de hash
Intuição
Inverta a abordagem e trabalhe com prefixos. Seja canEnd[i] uma indicação de se as primeiras i letras podem ser divididas em palavras. O prefixo vazio não precisa de palavras, então canEnd[0] é true. As primeiras end letras podem ser divididas exatamente quando a última parte delas, as letras de start a end, é uma palavra e as letras anteriores a ela podem ser divididas, isto é, canEnd[start] é true. Preencha a tabela da esquerda para a direita e todo canEnd[start] de que você precisa já será conhecido.
Em vez de comparar todas as m palavras em cada posição, coloque as palavras em um conjunto hash e procure as possíveis partes finais. Nenhuma palavra tem mais de L letras, então apenas as L partes que terminam em end podem corresponder. Em sunflowerseed, canEnd se torna true nas posições 0, 3 (sun), 7 (flow), 9 (flower) e 13 (seed após a posição 9), então a resposta é true. A posição 7 não leva a lugar nenhum, porque nenhuma palavra começa com er, e a tabela não se importa.
Há n posições, cada uma consulta no máximo L partes, e construir e calcular o hash de uma parte custa até L etapas. Isso dá O(n × L²), no máximo 300 × 20 × 20 = 1.2 × 10^5 etapas de processamento de letras, independentemente do tamanho do dicionário. A construção do conjunto lê cada palavra uma vez, O(m × L), então o total é O(m × L + n × L²). O conjunto armazena as palavras, O(m × L) letras, e a tabela armazena n + 1 sinalizadores. Não há recursão.
Algoritmo
- Coloque cada palavra em um conjunto hash e anote o comprimento
Lda palavra mais longa. - Crie
canEndcomn + 1entradas, todasfalse, e definacanEnd[0]comotrue. - Para cada
endde 1 an, experimente cadalengthde 1 amin(L, end). - Se
canEnd[end-length]fortruee o trecho desse comprimento que termina emendestiver no conjunto, definacanEnd[end]comotruee pare de testar comprimentos. - Retorne
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]
Armadilhas e casos extremos
A maioria das respostas erradas acontece por escolher um corte cedo demais ou por fazer uma busca que nunca se lembra das falhas.
- Cortar de forma gulosa. Escolher primeiro a palavra mais longa divide
carpetalemcarpete deixaal, emboracar+petalfuncione. Escolher primeiro a mais curta falha comsunflowerseed. - Verificar apenas se cada letra de
saparece em alguma palavra. Com as palavrasaaaaeaa, cada trecho tem comprimento par, entãoaaaaaaa, com sete letras, não pode ser dividido. - Armazenar no memo apenas as respostas
true. Umtrueencerra a busca de qualquer forma. O trabalho repetido ocorre nos ramosfalse, então um memo sem eles continua tendo complexidade exponencial. - Criar a tabela com uma entrada a menos.
canEnd[i]diz respeito às primeirasiletras, e tanto 0 quantonsão válidos, então são necessáriasn + 1entradas. - Comparar além do fim de
squando uma palavra é mais longa do que o trecho restante, como a palavraabcem relação aab. Verifique os comprimentos antes de comparar as letras. - Em Lua e R, as posições das strings começam em 1: um trecho de comprimento
kque termina na letraecomeça na letrae-k+1.
Perguntas frequentes4
Qual é a complexidade de tempo de Word Break?
A tabela de baixo para cima com um conjunto hash tem complexidade de tempo O(m × L + n × L²), em que n é o comprimento de s, m é o número de palavras e L é a palavra mais longa. A construção do conjunto lê cada palavra uma vez, e cada uma das n posições consulta no máximo L partes de até L letras. Se você comparar cada palavra em cada posição, em vez disso, a complexidade será O(n × m × L). A recursão simples sem memoização tem complexidade exponencial.
Por que uma abordagem gulosa falha no Word Break?
Uma regra gulosa se compromete com uma palavra e nunca reconsidera essa escolha. A estratégia de escolher primeiro a palavra mais longa divide carpetal em carpet e al, enquanto car + petal funciona. A estratégia de escolher primeiro a palavra mais curta divide sunflowerseed em sun + flow e fica presa em erseed. A programação dinâmica mantém todas as posições que podem ser alcançadas por algum corte, então nunca perde a posição correta.
Word Break é um problema de programação dinâmica ou de grafos?
As duas perspectivas funcionam. Na programação dinâmica, canEnd[i] indica se as primeiras i letras podem ser divididas, com base em prefixos menores. Como grafo, cada índice é um nó, com uma aresta de i a j quando as letras de i a j formam uma palavra, e você pergunta se o nó n é alcançável a partir do nó 0. Uma busca em largura com um conjunto de visitados faz o mesmo trabalho que a tabela.
Como listar cada frase em vez de retornar verdadeiro ou falso?
Use retrocesso: em cada índice, tente todas as palavras que se encaixam e faça uma chamada recursiva para o restante, construindo a frase conforme avança. Lembre-se da lista de frases para cada índice, para que o restante seja resolvido uma única vez. Execute primeiro a tabela de verdadeiro ou falso, para que uma string que não possa ser dividida ignore a busca. O número de frases pode crescer exponencialmente, então o tamanho da saída determina o tempo de execução.
Problemas parecidos
Problemas que usam as mesmas ideias. Resolver dois ou três é o que fixa um padrão.
Python
def wordBreak(s, wordDict):
# Escreva o código aquiCaso 1
Caso 2
Caso 3
Entrada
s = "sunflowerseed" wordDict = ["sun", "flow", "flower", "seed"]
Esperado
true