0Pricing
DSA Interview Prep · Lezione

Word Break e segmentazione delle stringhe

Usi una tabella DP 1D per determinare se una stringa può essere segmentata in parole del dizionario, analizzando il tempo O(n²) e il motivo per cui un trie lo accelera

Word Break e segmentazione delle stringhe è una lezione DSA Interview Prep gratuita su CoddyKit. Questa è la lezione 3 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento DSA Interview Prep, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso DSA Interview Prep include 4 lezioni in totale.

Il problema Word Break

Word Break (LeetCode 139) chiede, data una stringa s e un dizionario di parole, di determinare se s può essere segmentata in una sequenza separata da spazi composta da una o più parole del dizionario. Ad esempio, con s = 'leetcode' e wordDict = ['leet', 'code'], la risposta è True perché 'leet' + 'code' = 'leetcode'. Questo è un classico problema di DP 1D.

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')

Formulazione e stato della DP

Definisca dp[i] come True se la sottostringa s[:i] può essere segmentata usando il dizionario. Il caso base è dp[0] = True; la stringa vuota può sempre essere segmentata. Per ogni posizione i, verifichi tutte le posizioni j < i: se dp[j] è True e s[j:i] appartiene al dizionario, allora dp[i] = True. La risposta finale è 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

Esecuzione passo per passo della tabella DP

Per s = 'leetcode' e dict {'leet', 'code'}: dp[0]=T. Per i=4: j=0, dp[0]=T e s[0:4]='leet' appartiene a dict → dp[4]=T. Per i=8: j=4, dp[4]=T e s[4:8]='code' appartiene a dict → dp[8]=T. Tutte le altre posizioni in cui non termina alcuna parola rimangono False. La risposta dp[8]=True conferma che la stringa è segmentabile.

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

Analisi della complessità temporale

La DP ingenua richiede un tempo O(n²): n iterazioni esterne moltiplicate per un massimo di n iterazioni interne. Tuttavia, anche il sezionamento di s[j:i] ha un costo O(n), quindi la complessità effettiva in Python è O(n³). Un'ottimizzazione consiste nell'iterare sulle parole del dizionario e verificare se ogni parola termina nella posizione i, ottenendo O(n × W × L), dove W è la dimensione del dizionario e L la lunghezza media delle parole. Per la maggior parte degli input dei colloqui, O(n²) o O(n³) è accettabile.

# 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: ricorsione con memoizzazione

Lo stesso problema può essere risolto dall'alto verso il basso con la memoizzazione. Definisca una funzione ricorsiva can_break(start) che restituisce True se s[start:] è segmentabile. Provi ogni parola come prefisso di s[start:] e ricorra sul resto della stringa. Memorizzi i risultati per evitare di riesaminare più volte lo stesso indice iniziale. Questo approccio è equivalente alla DP dal basso verso l'alto, ma nella pratica può essere più veloce se molte posizioni vengono escluse in anticipo.

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

Restituire tutte le segmentazioni valide

Word Break II (LeetCode 140) richiede tutte le segmentazioni possibili. L'approccio consiste nel backtracking con memoizzazione: si ricorre da ogni posizione e, quando una parola corrisponde, si ricorre sul resto della stringa. Si memorizzano tutti i risultati parziali come liste di stringhe. Per evitare il TLE, si memorizzi l'elenco delle frasi possibili a partire da ogni indice iniziale. Il numero di frasi può essere esponenziale nel caso peggiore, ma la memoizzazione elimina i calcoli ridondanti.

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']

Ottimizzazione con Trie

Quando il dizionario è grande o le parole sono lunghe, verificare s[j:i] in word_set per ogni j è lento a causa dell'hashing delle stringhe di Python. Un Trie consente di attraversare la struttura carattere per carattere, eliminando presto i percorsi impossibili. Invece di verificare tutte le O(n) posizioni iniziali, si seguono soltanto i percorsi presenti nel Trie. Questo riduce significativamente il tempo di esecuzione effettivo quando pochi prefissi portano a parole valide.

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

Casi limite e vincoli

Casi limite importanti: (1) Stringa vuota: restituisce True, perché una stringa vuota è banalmente segmentabile. (2) Parola non presente nel dizionario: dp non imposta mai a True la posizione corrispondente e restituisce correttamente False. (3) Parole sovrapposte: ad esempio, 'a' e 'aa' nel dict con s='aaa'; la DP gestisce naturalmente il caso verificando tutti i valori di j. (4) Caratteri ripetuti: s='aaaaab' con dict=['a','aa','aaa']; i percorsi sono esponenziali, ma la memoizzazione li limita 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)

Generalizzazione della segmentazione di stringhe

Word Break si generalizza a qualsiasi problema di segmentazione di stringhe: è possibile suddividere la stringa s secondo una determinata regola? Sostituisca la ricerca nel dizionario con un controllo O(1) o O(L). Per esempio, è possibile suddividere s in palindromi? Utilizzi una tabella dei palindromi precalcolata invece di un insieme di parole. La struttura della DP è identica: cambia soltanto il controllo di validità.

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)

Approccio DP vs BFS

Word Break può anche essere formulato come un problema di percorso minimo con BFS: ogni posizione nella stringa è un nodo e c'è un arco da j a i se s[j:i] appartiene al dizionario. Eseguire la BFS dal nodo 0 significa verificare se il nodo n è raggiungibile. La BFS offre la stessa complessità O(n² × L), ma può risultare più intuitiva se durante un colloquio si modella il problema come un grafo.

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

Strategia di comunicazione durante il colloquio

Durante un colloquio, esponga questo ragionamento: (1) osservi che le scelte in ogni posizione dipendono da ciò che era raggiungibile in precedenza: questo indica la DP; (2) definisca lo stato: dp[i] = è possibile segmentare s[:i]?; (3) enunci la ricorrenza e il caso base prima di scrivere il codice; (4) scriva prima la soluzione O(n²), poi citi l'ottimizzazione con Trie come possibile approfondimento; (5) discuta i casi limite: stringa vuota, singolo carattere, parola non presente nel dizionario.

# 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 rapida

Metta alla prova la Sua comprensione dei concetti di Data Structures & Algorithms — Coding Interview Prep trattati in questa lezione.

Riepilogo della lezione

In questa lezione ha imparato: dp[i] rappresenta se s[:i] può essere segmentata in parole del dizionario, la ricorrenza O(n²) verifica tutti i punti di divisione j per cui dp[j]=True e s[j:i] appartiene all'insieme di parole e un Trie può accelerare il ciclo interno eliminando presto i prefissi inesistenti. Nella prossima lezione esamineremo Decode Ways e il conteggio dei percorsi, un altro schema di DP 1D simile a Fibonacci.

Domande Frequenti

La lezione «Word Break e segmentazione delle stringhe» è gratuita?

Sì — il testo completo di «Word Break e segmentazione delle stringhe» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso DSA Interview Prep, passa a CoddyKit PRO. Il corso DSA Interview Prep include 4 lezioni in totale.

Cosa imparerò in «Word Break e segmentazione delle stringhe»?

Usi una tabella DP 1D per determinare se una stringa può essere segmentata in parole del dizionario, analizzando il tempo O(n²) e il motivo per cui un trie lo accelera Eserciti DSA Interview Prep con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare DSA Interview Prep?

Non è richiesta alcuna esperienza precedente. DSA Interview Prep su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 3 di 4.

Quanto tempo richiede la lezione «Word Break e segmentazione delle stringhe»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione DSA Interview Prep?

Sì. Ogni lezione DSA Interview Prep include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. House Robber: ricorrenza prendi o salta
  2. Subarray a somma massima e subarray a prodotto massimo
  3. Word Break e segmentazione delle stringhe
  4. Decode Ways e conteggio dei percorsi
← Torna a DSA Interview Prep