Word Break et segmentation de chaînes
Utilisez un tableau de DP unidimensionnel pour déterminer si une chaîne peut être segmentée en mots du dictionnaire, analysez le temps en O(n²) et découvrez pourquoi un trie l’accélère.
Word Break et segmentation de chaînes est une leçon Coding Interview Prep gratuite sur CoddyKit. Ceci est la leçon 3 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage Coding Interview Prep, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours Coding Interview Prep comprend 4 leçons au total.
Le problème de la segmentation de mots
Segmentation de mots (LeetCode 139) demande, étant donné une chaîne s et un dictionnaire de mots, de déterminer si s peut être segmentée en une séquence séparée par des espaces contenant un ou plusieurs mots du dictionnaire. Par exemple, avec s = 'leetcode' et wordDict = ['leet', 'code'], la réponse est True car 'leet' + 'code' = 'leetcode'. Il s'agit d'un problème classique de DP à une dimension.
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')Formulation et état de DP
Définissez dp[i] comme valant True si la sous-chaîne s[:i] peut être segmentée à l'aide du dictionnaire. Le cas de base est dp[0] = True (la chaîne vide peut toujours être segmentée). Pour chaque position i, vérifiez toutes les positions j < i : si dp[j] vaut True et si s[j:i] appartient au dictionnaire, alors dp[i] = True. La réponse finale est 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'])) # FalseParcours de la table de DP
Pour s = 'leetcode' et le dictionnaire {'leet', 'code'} : dp[0]=T. Pour i=4 : j=0, dp[0]=T et s[0:4]='leet' appartient au dictionnaire → dp[4]=T. Pour i=8 : j=4, dp[4]=T et s[4:8]='code' appartient au dictionnaire → dp[8]=T. Toutes les autres positions où aucun mot ne se termine restent fausses. La réponse dp[8]=True confirme que la chaîne peut être segmentée.
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'])Analyse de la complexité temporelle
La DP naïve s'exécute en temps O(n²) : n itérations externes, chacune comportant jusqu'à n itérations internes. Cependant, la création de la sous-chaîne s[j:i] coûte également O(n), ce qui porte la complexité réelle à O(n³) en Python. Une optimisation consiste à parcourir les mots du dictionnaire et à vérifier si chaque mot se termine à la position i, ce qui donne O(n × W × L), où W est la taille du dictionnaire et L la longueur moyenne des mots. Pour la plupart des données d'entrée d'entretien, O(n²) ou O(n³) est acceptable.
# 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'])) # TrueAlternative : récursion avec mémoïsation
Le même problème peut être résolu de manière descendante avec une mémoïsation. Définissez une fonction récursive can_break(start) qui renvoie True si s[start:] peut être segmentée. Essayez chaque mot comme préfixe de s[start:], puis appelez récursivement la fonction sur le reste. Mémorisez les résultats afin d'éviter d'explorer plusieurs fois le même indice de départ. Cette approche est équivalente à la DP ascendante, mais peut être plus rapide en pratique si de nombreuses positions sont éliminées tôt.
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'])) # FalseRenvoyer toutes les segmentations valides
Segmentation de mots II (LeetCode 140) demande toutes les segmentations possibles. L'approche repose sur une recherche avec retour arrière et une mémoïsation : partez récursivement de chaque position et, lorsqu'un mot correspond, poursuivez la récursion sur le reste. Stockez tous les résultats partiels sous forme de listes de chaînes. Pour éviter un TLE, mémorisez la liste des phrases possibles à partir de chaque indice de départ. Le nombre de phrases peut être exponentiel dans le pire des cas, mais la mémoïsation élimine les calculs redondants.
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']Optimisation par arbre préfixe
Lorsque le dictionnaire est volumineux ou que les mots sont longs, vérifier s[j:i] in word_set pour toutes les valeurs de j est lent en raison du hachage des chaînes en Python. Un arbre préfixe vous permet de parcourir l'arbre caractère par caractère et d'éliminer rapidement les chemins impossibles. Au lieu de vérifier les O(n) positions de départ, vous ne suivez que les chemins qui existent dans l'arbre. Cela réduit considérablement le temps d'exécution en pratique lorsque peu de préfixes mènent à des mots valides.
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'])) # TrueCas limites et contraintes
Cas limites importants : (1) Chaîne vide : renvoyez True (une chaîne vide peut être segmentée de manière triviale). (2) Mot absent du dictionnaire : la DP ne définit jamais la position correspondante à True et renvoie donc correctement False. (3) Mots qui se chevauchent : par exemple, avec 'a' et 'aa' dans le dictionnaire et la chaîne 'aaa', la DP gère naturellement ce cas en vérifiant toutes les valeurs de j. (4) Caractères répétés : pour la chaîne 'aaaaab' avec le dictionnaire ['a','aa','aaa'], le nombre de chemins est exponentiel, mais la mémoïsation le plafonne à 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)Généralisation de la segmentation de chaînes
La segmentation de mots se généralise à tout problème de segmentation de chaînes : une chaîne s peut-elle être partitionnée selon une règle donnée ? Remplacez la recherche dans le dictionnaire par toute vérification en O(1) ou en O(L). Par exemple, une chaîne s peut-elle être partitionnée en palindromes ? Utilisez une table de palindromes précalculée plutôt qu'un ensemble de mots. La structure de la DP est identique : seule la vérification de validité change.
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)DP ou approche BFS
La segmentation de mots peut également être formulée comme un problème de plus court chemin avec BFS : chaque position de la chaîne est un nœud, et une arête relie j à i si s[j:i] appartient au dictionnaire. Un BFS depuis le nœud 0 demande si le nœud n est accessible. Le BFS donne la même complexité O(n² × L), mais peut être plus intuitif si vous le modélisez comme un problème de graphe pendant un entretien.
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'])) # FalseStratégie de communication en entretien
Lors d'un entretien, présentez votre raisonnement étape par étape : (1) Observez que les choix effectués à chaque position dépendent de ce qui était accessible auparavant — cela indique une DP. (2) Définissez l'état : dp[i] = peut-on segmenter s[:i] ? (3) Énoncez la récurrence et le cas de base avant de coder. (4) Codez d'abord la solution en O(n²), puis mentionnez l'optimisation par arbre préfixe comme prolongement possible. (5) Discutez des cas limites : chaîne vide, caractère unique, mot absent du dictionnaire.
# 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'])) # TrueVérification rapide
Évaluez votre compréhension des concepts de structures de données et d'algorithmes — préparation aux entretiens de programmation présentés dans cette leçon.
Récapitulatif de la leçon
Dans cette leçon, vous avez appris : dp[i] indique si s[:i] peut être segmentée en mots du dictionnaire, la récurrence en O(n²) vérifie tous les points de séparation j tels que dp[j]=True et s[j:i] appartient à l'ensemble de mots, et un arbre préfixe peut accélérer la boucle interne en éliminant rapidement les préfixes inexistants. Ensuite, nous étudierons le décodage et le comptage de chemins, un autre schéma de DP à une dimension semblable à celui de Fibonacci.
Questions Fréquemment Posées
La leçon « Word Break et segmentation de chaînes » est-elle gratuite ?
Oui — le texte complet de « Word Break et segmentation de chaînes » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours Coding Interview Prep, passe à CoddyKit PRO. Le cours Coding Interview Prep comprend 4 leçons au total.
Qu'est-ce que j'apprendrai dans « Word Break et segmentation de chaînes » ?
Utilisez un tableau de DP unidimensionnel pour déterminer si une chaîne peut être segmentée en mots du dictionnaire, analysez le temps en O(n²) et découvrez pourquoi un trie l’accélère. Tu pratiques Coding Interview Prep avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.
Dois-je avoir de l'expérience pour commencer Coding Interview Prep ?
Aucune expérience préalable n'est requise. Coding Interview Prep sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 3 sur 4.
Combien de temps prend la leçon « Word Break et segmentation de chaînes » ?
La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.
Peux-tu écrire et exécuter du code dans cette leçon Coding Interview Prep ?
Oui. Chaque leçon Coding Interview Prep inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.
Toutes les leçons de ce cours
- House Robber : récurrence prendre ou ignorer
- Sous-tableau de somme maximale et sous-tableau de produit maximal
- Word Break et segmentation de chaînes
- Décoder des façons et compter des chemins