0Pricing
Coding Interview Prep · Leçon

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']))  # False

Parcours 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']))  # True

Alternative : 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']))  # False

Renvoyer 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']))  # True

Cas 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']))  # False

Straté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']))  # True

Vé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

  1. House Robber : récurrence prendre ou ignorer
  2. Sous-tableau de somme maximale et sous-tableau de produit maximal
  3. Word Break et segmentation de chaînes
  4. Décoder des façons et compter des chemins
← Retour à Coding Interview Prep