0Pricing
DSA Interview Prep · Aula

Quebra de Palavras e Segmentação de Strings

Use uma tabela DP unidimensional para determinar se uma string pode ser segmentada em palavras do dicionário, analisando o tempo O(n²) e por que um trie o acelera.

Quebra de Palavras e Segmentação de Strings é uma aula grátis de DSA Interview Prep no CoddyKit. Esta é a aula 3 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de DSA Interview Prep, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de DSA Interview Prep inclui 4 aulas no total.

Problema de segmentação de palavras

Segmentação de palavras (LeetCode 139) solicita: dada uma cadeia s e um dicionário de palavras, determine se s pode ser segmentada em uma sequência separada por espaços de uma ou mais palavras do dicionário. Por exemplo, com s = 'leetcode' e wordDict = ['leet', 'code'], a resposta é True porque 'leet' + 'code' = 'leetcode'. Este é um problema clássico de DP unidimensional.

s = 'leetcode'
word_set = {'leet', 'code'}
# Can we split 'leetcode' into words from word_set?
# 'leet' in set → yes, 'code' in set → yes
# So: 'leetcode' = 'leet' + 'code' → True

s2 = 'catsandog'
word_set2 = {'cats', 'dog', 'sand', 'and', 'cat'}
# No matter how we split, last part 'og' not in dict
print('Expected: True, False')

Formulação e estado da DP

Defina dp[i] como True se a subcadeia s[:i] puder ser segmentada usando o dicionário. O caso-base é dp[0] = True (a cadeia vazia sempre pode ser segmentada). Para cada posição i, verifique todas as posições j < i: se dp[j] for True e s[j:i] estiver no dicionário, então dp[i] = True. A resposta final é dp[len(s)].

def word_break(s, word_dict):
    word_set = set(word_dict)
    n = len(s)
    dp = [False] * (n + 1)
    dp[0] = True  # empty string
    
    for i in range(1, n + 1):
        for j in range(i):
            # If s[:j] is segmentable AND s[j:i] is a word
            if dp[j] and s[j:i] in word_set:
                dp[i] = True
                break  # no need to check other j values
    return dp[n]

print(word_break('leetcode', ['leet', 'code']))        # True
print(word_break('catsandog', ['cats','dog','sand','and','cat']))  # False

Acompanhando a tabela de DP

Para s = 'leetcode' e o dicionário {'leet', 'code'}: dp[0]=T. Em i=4: j=0, dp[0]=T e s[0:4]='leet' está no dicionário → dp[4]=T. Em i=8: j=4, dp[4]=T e s[4:8]='code' está no dicionário → dp[8]=T. Todas as outras posições nas quais nenhuma palavra termina permanecem False. A resposta dp[8]=True confirma que a cadeia pode ser segmentada.

def word_break_trace(s, word_dict):
    word_set = set(word_dict)
    n = len(s)
    dp = [False] * (n + 1)
    dp[0] = True
    for i in range(1, n + 1):
        for j in range(i):
            if dp[j] and s[j:i] in word_set:
                dp[i] = True
                print(f'dp[{i}]=True via s[{j}:{i}]={repr(s[j:i])}')
                break
    print('dp table:', dp)
    return dp[n]

word_break_trace('leetcode', ['leet', 'code'])

Análise da complexidade temporal

A DP ingênua é executada em O(n²) de tempo: n iterações externas vezes até n iterações internas. No entanto, o fatiamento de s[j:i] também custa O(n), fazendo com que a complexidade real seja O(n³) em Python. Uma otimização consiste em percorrer as palavras do dicionário e verificar se cada palavra termina na posição i, obtendo O(n × W × L), em que W é o tamanho do dicionário e L é o comprimento médio das palavras. Para a maioria das entradas de entrevistas, O(n²) ou O(n³) é aceitável.

# Slightly faster: iterate over words rather than all j positions
def word_break_v2(s, word_dict):
    word_set = set(word_dict)
    n = len(s)
    dp = [False] * (n + 1)
    dp[0] = True
    for i in range(1, n + 1):
        for word in word_set:
            wl = len(word)
            # Does 'word' end exactly at position i?
            if i >= wl and dp[i - wl] and s[i - wl:i] == word:
                dp[i] = True
                break
    return dp[n]

print(word_break_v2('applepenapple', ['apple', 'pen']))  # True

Alternativa de recursão com memoização

O mesmo problema pode ser resolvido de cima para baixo com memoização. Defina uma função recursiva can_break(start) que retorne verdadeiro se s[start:] puder ser segmentada. Tente cada palavra como prefixo de s[start:] e faça uma chamada recursiva para o restante. Guarde os resultados para evitar explorar novamente o mesmo índice inicial várias vezes. Isso equivale à DP de baixo para cima, mas pode ser mais rápido na prática se muitas posições forem descartadas logo no início.

from functools import lru_cache

def word_break_memo(s, word_dict):
    word_set = set(word_dict)
    
    @lru_cache(maxsize=None)
    def can_break(start):
        if start == len(s): return True
        for end in range(start + 1, len(s) + 1):
            if s[start:end] in word_set and can_break(end):
                return True
        return False
    
    return can_break(0)

print(word_break_memo('leetcode', ['leet', 'code']))  # True
print(word_break_memo('catsandog', ['cats','dog','sand','and','cat']))  # False

Retornando todas as segmentações válidas

Segmentação de palavras II (LeetCode 140) solicita todas as segmentações possíveis. A abordagem usa busca com retrocesso e memoização: faça uma chamada recursiva a partir de cada posição e, quando uma palavra corresponder, faça outra chamada para o restante. Armazene todos os resultados parciais como listas de cadeias de caracteres. Para evitar TLE, memorize a lista de frases possíveis a partir de cada índice inicial. O número de frases pode ser exponencial no pior caso, mas a memoização elimina cálculos redundantes.

from functools import lru_cache

def word_break_ii(s, word_dict):
    word_set = set(word_dict)
    
    @lru_cache(maxsize=None)
    def break_from(start):
        if start == len(s): return ['']
        results = []
        for end in range(start + 1, len(s) + 1):
            word = s[start:end]
            if word in word_set:
                for rest in break_from(end):
                    results.append(word if not rest else word + ' ' + rest)
        return results
    
    return break_from(0)

print(word_break_ii('catsanddog', ['cat','cats','and','sand','dog']))
# ['cat sand dog', 'cats and dog']

Otimização com árvore de prefixos

Quando o dicionário é grande ou as palavras são longas, verificar s[j:i] in word_set para todo j é lento devido ao cálculo de hash das cadeias de caracteres em Python. Uma árvore de prefixos permite percorrer a árvore caractere a caractere, eliminando cedo os caminhos impossíveis. Em vez de verificar todas as O(n) posições iniciais, você segue apenas os caminhos que existem na árvore. Isso reduz significativamente o tempo de execução na prática quando poucos prefixos levam a palavras válidas.

class TrieNode:
    def __init__(self):
        self.children = {}
        self.is_end = False

def build_trie(words):
    root = TrieNode()
    for word in words:
        node = root
        for ch in word:
            node = node.children.setdefault(ch, TrieNode())
        node.is_end = True
    return root

def word_break_trie(s, word_dict):
    root = build_trie(word_dict)
    n = len(s)
    dp = [False] * (n + 1)
    dp[0] = True
    for i in range(n):
        if not dp[i]: continue
        node = root
        for j in range(i, n):
            ch = s[j]
            if ch not in node.children: break
            node = node.children[ch]
            if node.is_end:
                dp[j + 1] = True
    return dp[n]

print(word_break_trie('leetcode', ['leet', 'code']))  # True

Casos-limite e restrições

Casos-limite importantes: (1) Cadeia vazia: retorne verdadeiro (a cadeia vazia pode ser segmentada trivialmente). (2) Palavra ausente do dicionário: dp nunca define a posição correspondente como verdadeira e retorna falso corretamente. (3) Palavras sobrepostas: por exemplo, 'a' e 'aa' no dicionário, com s='aaa' — a DP trata isso naturalmente ao verificar todos os valores de j. (4) Caracteres repetidos: s='aaaaab' com o dicionário contendo ['a','aa','aaa'] — há caminhos exponenciais, mas a memoização limita isso a O(n²).

def word_break(s, word_dict):
    word_set = set(word_dict)
    dp = [False] * (len(s) + 1)
    dp[0] = True
    for i in range(1, len(s) + 1):
        for j in range(i):
            if dp[j] and s[j:i] in word_set:
                dp[i] = True
                break
    return dp[len(s)]

# Edge cases
print(word_break('', ['hello']))          # True (empty string)
print(word_break('a', ['b']))             # False
print(word_break('aaa', ['a', 'aa']))     # True (many ways)

Generalização da segmentação de cadeias de caracteres

A segmentação de palavras generaliza-se para qualquer problema de segmentação de cadeias de caracteres: a cadeia s pode ser particionada segundo alguma regra? Substitua a consulta ao dicionário por uma verificação O(1) ou O(L). Por exemplo: s pode ser particionada em palíndromos? Use uma tabela de palíndromos pré-computada em vez de um conjunto de palavras. A estrutura da DP é idêntica — apenas a verificação de validade muda.

def palindrome_partition_possible(s):
    '''Can s be partitioned into palindromes? (Always yes — single chars are palindromes)'''
    n = len(s)
    # Precompute palindrome table
    is_pal = [[False]*n for _ in range(n)]
    for i in range(n): is_pal[i][i] = True
    for i in range(n-1): is_pal[i][i+1] = (s[i]==s[i+1])
    for length in range(3, n+1):
        for i in range(n-length+1):
            j = i + length - 1
            is_pal[i][j] = s[i]==s[j] and is_pal[i+1][j-1]
    # DP similar to word break
    dp = [False] * (n + 1)
    dp[0] = True
    for i in range(1, n + 1):
        for j in range(i):
            if dp[j] and is_pal[j][i-1]:
                dp[i] = True
                break
    return dp[n]

print(palindrome_partition_possible('aab'))  # True (a,a,b or aa,b)

Abordagem DP versus BFS

A segmentação de palavras também pode ser formulada como um problema de caminho mínimo por BFS: cada posição na cadeia é um nó, e existe uma aresta de j para i se s[j:i] estiver no dicionário. A BFS a partir do nó 0 verifica se o nó n é alcançável. A BFS apresenta a mesma complexidade O(n² × L), mas pode ser mais intuitiva se você modelar o problema como um problema de grafos durante uma entrevista.

from collections import deque

def word_break_bfs(s, word_dict):
    word_set = set(word_dict)
    n = len(s)
    visited = set()
    queue = deque([0])
    while queue:
        start = queue.popleft()
        if start == n: return True
        for end in range(start + 1, n + 1):
            if end not in visited and s[start:end] in word_set:
                visited.add(end)
                queue.append(end)
    return False

print(word_break_bfs('leetcode', ['leet', 'code']))    # True
print(word_break_bfs('catsandog', ['cats','dog','and','sand','cat']))  # False

Estratégia de comunicação em entrevistas

Em uma entrevista, percorra este raciocínio: (1) Observe que as escolhas em cada posição dependem do que era alcançável anteriormente — isso indica DP. (2) Defina o estado: dp[i] = podemos segmentar s[:i]? (3) Declare a recorrência e o caso-base antes de programar. (4) Primeiro programe a solução O(n²), depois mencione a otimização com árvore de prefixos como continuação. (5) Discuta os casos-limite: cadeia vazia, caractere único, palavra ausente do dicionário.

# Clean final solution to present in interview
def word_break(s, word_dict):
    '''O(n^2 * L) time, O(n + W) space where W = total word length in dict'''
    word_set = set(word_dict)   # O(W) space
    n = len(s)
    dp = [False] * (n + 1)     # O(n) space
    dp[0] = True
    for i in range(1, n + 1):
        for j in range(i):     # try all split points
            if dp[j] and s[j:i] in word_set:
                dp[i] = True
                break
    return dp[n]

# Time: O(n^2 * L) - n^2 pairs, each dict lookup is O(L)
# Space: O(n) for dp array, O(W) for word_set
print(word_break('applepenapple', ['apple', 'pen']))  # True

Verificação rápida

Teste sua compreensão dos conceitos de Estruturas de Dados e Algoritmos — Preparação para Entrevistas de Programação desta lição.

Recapitulação da lição

Nesta lição, você aprendeu: dp[i] representa se s[:i] pode ser segmentada em palavras do dicionário, a recorrência O(n²) verifica todos os pontos de divisão j em que dp[j]=True e s[j:i] está no conjunto de palavras e uma árvore de prefixos pode acelerar o laço interno ao eliminar cedo os prefixos inexistentes. Em seguida, exploraremos as Formas de Decodificação e a Contagem de Caminhos, outro padrão de DP unidimensional semelhante ao de Fibonacci.

Perguntas Frequentes

A aula “Quebra de Palavras e Segmentação de Strings” é grátis?

Sim — o texto completo de “Quebra de Palavras e Segmentação de Strings” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de DSA Interview Prep, atualize para CoddyKit PRO. O curso de DSA Interview Prep inclui 4 aulas no total.

O que vou aprender em “Quebra de Palavras e Segmentação de Strings”?

Use uma tabela DP unidimensional para determinar se uma string pode ser segmentada em palavras do dicionário, analisando o tempo O(n²) e por que um trie o acelera. Você pratica DSA Interview Prep com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar DSA Interview Prep?

Nenhuma experiência prévia é necessária. DSA Interview Prep no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 3 de 4.

Quanto tempo leva a aula “Quebra de Palavras e Segmentação de Strings”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de DSA Interview Prep?

Sim. Cada aula de DSA Interview Prep inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. House Robber: Recorrência Escolher ou Ignorar
  2. Subarray Máximo e Subarray de Produto Máximo
  3. Quebra de Palavras e Segmentação de Strings
  4. Decodificando Formas e Contando Caminhos
← Voltar para DSA Interview Prep