0Pricing
DSA Interview Prep · Lekcja

Word Break i dzielenie ciągu na segmenty

Wykorzystają Państwo jednowymiarową tablicę DP, aby ustalić, czy ciąg można podzielić na słowa ze słownika, przeanalizują czas O(n²) i zobaczą, dlaczego trie go przyspiesza.

Word Break i dzielenie ciągu na segmenty to bezpłatna lekcja DSA Interview Prep na CoddyKit. To lekcja 3 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej DSA Interview Prep, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs DSA Interview Prep zawiera 4 lekcji w sumie.

Problem Word Break

Word Break (LeetCode 139) polega na tym, aby dla danego napisu s i słownika słów ustalić, czy napis s można podzielić na oddzieloną spacjami sekwencję składającą się z co najmniej jednego słowa ze słownika. Na przykład dla s = 'leetcode' i wordDict = ['leet', 'code'] odpowiedź to True, ponieważ 'leet' + 'code' = 'leetcode'. Jest to klasyczny problem jednowymiarowego DP.

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

Sformułowanie DP i stan

Zdefiniuj dp[i] jako True, jeśli podnapis s[:i] można podzielić przy użyciu słownika. Przypadkiem bazowym jest dp[0] = True (pusty napis zawsze można podzielić). Dla każdej pozycji i sprawdź wszystkie pozycje j < i: jeśli dp[j] ma wartość True, a s[j:i] znajduje się w słowniku, ustaw dp[i] = True. Ostateczną odpowiedzią jest 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

Śledzenie tabeli DP

Dla s = 'leetcode' i słownika {'leet', 'code'}: dp[0]=T. Dla i=4: j=0, dp[0]=T, a s[0:4]='leet' znajduje się w słowniku → dp[4]=T. Dla i=8: j=4, dp[4]=T, a s[4:8]='code' znajduje się w słowniku → dp[8]=T. Wszystkie pozostałe pozycje, na których nie kończy się żadne słowo, pozostają False. Odpowiedź dp[8]=True potwierdza, że napis można podzielić.

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

Analiza złożoności czasowej

Naiwna wersja DP działa w czasie O(n²): wykonuje n iteracji zewnętrznych, a w każdej z nich do n iteracji wewnętrznych. Jednak wycinanie fragmentu s[j:i] również kosztuje O(n), więc rzeczywista złożoność w Pythonie wynosi O(n³). Jedną z optymalizacji jest iterowanie po słowach w słowniku i sprawdzanie, czy każde słowo kończy się na pozycji i, co daje O(n × W × L), gdzie W oznacza rozmiar słownika, a L — średnią długość słowa. W przypadku większości danych wejściowych z rozmów rekrutacyjnych O(n²) lub O(n³) jest akceptowalne.

# 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

Alternatywa: rekurencja z memoizacją

Ten sam problem można rozwiązać odgórnie za pomocą memoizacji. Zdefiniuj rekurencyjną funkcję can_break(start), która zwraca True, jeśli s[start:] można podzielić. Spróbuj użyć każdego słowa jako prefiksu s[start:] i wywołaj rekurencję dla pozostałej części. Zapamiętuj wyniki, aby uniknąć wielokrotnego analizowania tego samego indeksu początkowego. Podejście to jest równoważne oddolnemu DP, ale w praktyce może działać szybciej, jeśli wiele pozycji zostanie wcześnie odrzuconych.

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

Zwracanie wszystkich poprawnych segmentacji

Word Break II (LeetCode 140) wymaga znalezienia wszystkich możliwych segmentacji. Podejście polega na rekurencji z nawrotami i memoizacją: wykonuj rekurencję od każdej pozycji, a gdy słowo pasuje, wywołuj rekurencję dla pozostałej części. Przechowuj wszystkie częściowe wyniki jako listy napisów. Aby uniknąć TLE, zapamiętuj listę zdań możliwych do utworzenia od każdego indeksu początkowego. W najgorszym przypadku liczba zdań może być wykładnicza, ale memoizacja eliminuje zbędne obliczenia.

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

Optymalizacja za pomocą Trie

Gdy słownik jest duży lub słowa są długie, sprawdzanie s[j:i] in word_set dla każdego j jest powolne z powodu haszowania napisów w Pythonie. Trie pozwala przechodzić przez kolejne znaki i wcześnie odrzucać niemożliwe ścieżki. Zamiast sprawdzać wszystkie O(n) pozycji początkowych, podążamy tylko ścieżkami istniejącymi w Trie. Znacznie skraca to czas działania w praktyce, gdy niewiele prefiksów prowadzi do poprawnych słów.

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

Przypadki brzegowe i ograniczenia

Ważne przypadki brzegowe: (1) Pusty napis: zwróć True (pusty napis można podzielić w oczywisty sposób). (2) Słowo spoza słownika: dp nigdy nie ustawi odpowiadającej mu pozycji na True, więc poprawnie zwróci False. (3) Nakładające się słowa: na przykład 'a' i 'aa' w słowniku przy s='aaa' — DP obsługuje to naturalnie, sprawdzając wszystkie wartości j. (4) Powtarzające się znaki: s='aaaaab' przy dict=['a','aa','aaa'] — liczba ścieżek rośnie wykładniczo, ale memoizacja ogranicza ją do 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)

Uogólnienie segmentacji napisu

Problem Word Break uogólnia się na dowolny problem segmentacji napisu: czy napis s można podzielić zgodnie z określoną regułą? Zastąp wyszukiwanie w słowniku dowolnym sprawdzeniem o złożoności O(1) lub O(L). Na przykład: czy s można podzielić na palindromy? Zamiast zbioru słów użyj wcześniej obliczonej tabeli palindromów. Struktura DP pozostaje identyczna — zmienia się tylko sprawdzanie poprawności.

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)

Podejście DP a BFS

Problem Word Break można również przedstawić jako problem najkrótszej ścieżki wyszukiwania BFS: każda pozycja w napisie jest węzłem, a między j i i istnieje krawędź, jeśli s[j:i] znajduje się w słowniku. Wyszukiwanie BFS od węzła 0 sprawdza, czy węzeł n jest osiągalny. BFS daje tę samą złożoność O(n² × L), ale może być bardziej intuicyjne, jeśli podczas rozmowy rekrutacyjnej potraktują Państwo problem jako zadanie grafowe.

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 komunikacji podczas rozmowy rekrutacyjnej

Podczas rozmowy rekrutacyjnej warto przejść przez następujący tok rozumowania: (1) Zauważyć, że wybory na każdej pozycji zależą od tego, co było wcześniej osiągalne — wskazuje to na DP. (2) Zdefiniować stan: dp[i] = czy można podzielić s[:i]? (3) Podać rekurencję i przypadek bazowy przed rozpoczęciem kodowania. (4) Najpierw napisać rozwiązanie O(n²), a następnie wspomnieć o optymalizacji za pomocą Trie. (5) Omówić przypadki brzegowe: pusty napis, pojedynczy znak, słowo spoza słownika.

# 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

Szybki test

Proszę sprawdzić, jak dobrze rozumieją Państwo zagadnienia Data Structures & Algorithms — Coding Interview Prep omówione w tej lekcji.

Podsumowanie lekcji

W tej lekcji nauczyli się Państwo: dp[i] określa, czy s[:i] można podzielić na słowa ze słownika, rekurencja O(n²) sprawdza wszystkie punkty podziału j, dla których dp[j]=True i s[j:i] znajduje się w zbiorze słów oraz Trie może przyspieszyć pętlę wewnętrzną, wcześnie odrzucając nieistniejące prefiksy. Następnie omówimy problem Decode Ways i zliczanie ścieżek — kolejny jednowymiarowy wzorzec DP przypominający ciąg Fibonacciego.

Często zadawane pytania

Czy lekcja „Word Break i dzielenie ciągu na segmenty” jest bezpłatna?

Tak — pełny tekst „Word Break i dzielenie ciągu na segmenty” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu DSA Interview Prep, przejdź na CoddyKit PRO. Kurs DSA Interview Prep zawiera 4 lekcji w sumie.

Co nauczysz się w „Word Break i dzielenie ciągu na segmenty”?

Wykorzystają Państwo jednowymiarową tablicę DP, aby ustalić, czy ciąg można podzielić na słowa ze słownika, przeanalizują czas O(n²) i zobaczą, dlaczego trie go przyspiesza. Ćwiczysz DSA Interview Prep z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć DSA Interview Prep?

Nie wymagamy żadnego doświadczenia. DSA Interview Prep w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 3 z 4.

Ile czasu zajmuje lekcja „Word Break i dzielenie ciągu na segmenty”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji DSA Interview Prep?

Tak. Każda lekcja DSA Interview Prep zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. House Robber: rekurencja wybierz albo pomiń
  2. Maksymalna podtablica i podtablica o maksymalnym iloczynie
  3. Word Break i dzielenie ciągu na segmenty
  4. Decode Ways i zliczanie ścieżek
← Powrót do DSA Interview Prep