0Pricing
Coding Interview Prep · Урок

Разбиение слов и сегментация строки

Используйте одномерную таблицу DP, чтобы определить, можно ли разбить строку на слова из словаря, анализируйте сложность O(n²) и узнайте, почему бор ускоряет решение

«Разбиение слов и сегментация строки» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Coding Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Coding Interview Prep содержит 4 уроков всего.

Задача о разбиении строки на слова

Разбиение строки на слова (LeetCode 139) — это задача, в которой по строке s и словарю слов нужно определить, можно ли разбить s на последовательность из одного или нескольких слов словаря, разделённых пробелами. Например, если s = 'leetcode' и wordDict = ['leet', 'code'], ответом будет True, поскольку 'leet' + 'code' = 'leetcode'. Это классическая задача с одномерной 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')

Формулировка и состояние DP

Определим dp[i] как True, если подстроку s[:i] можно разбить, используя словарь. Базовый случай: dp[0] = True (пустую строку всегда можно разбить). Для каждой позиции i проверьте все позиции j < i: если dp[j] равно True и s[j:i] содержится в словаре, то dp[i] = True. Итоговый ответ — 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

Трассировка таблицы DP

Для s = 'leetcode' и словаря {'leet', 'code'}: dp[0]=T. При i=4: j=0, dp[0]=T и s[0:4]='leet' содержится в словаре → dp[4]=T. При i=8: j=4, dp[4]=T и s[4:8]='code' содержится в словаре → dp[8]=T. Для всех остальных позиций, где не заканчивается ни одно слово, сохраняется значение False. Ответ dp[8]=True подтверждает, что строку можно разбить.

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

Анализ временной сложности

Наивная DP работает за O(n²) времени: выполняется n внешних итераций, каждая из которых содержит до n внутренних итераций. Однако получение среза s[j:i] также стоит O(n), поэтому фактическая сложность в Python составляет O(n³). Один из вариантов оптимизации — перебирать слова в словаре и проверять, заканчивается ли каждое слово в позиции i, что даёт O(n × W × L), где W — размер словаря, а L — средняя длина слова. Для большинства входных данных на собеседованиях достаточно сложности O(n²) или O(n³).

# 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

Альтернатива: рекурсия с мемоизацией

Ту же задачу можно решить сверху вниз с помощью мемоизации. Определите рекурсивную функцию can_break(start), которая возвращает True, если s[start:] можно разбить. Переберите каждое слово как префикс s[start:] и рекурсивно обработайте оставшуюся часть. Кэшируйте результаты, чтобы не исследовать один и тот же начальный индекс несколько раз. Это эквивалентно DP снизу вверх, но на практике может работать быстрее, если многие позиции удаётся рано исключить.

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

Возврат всех допустимых разбиений

Разбиение строки на слова II (LeetCode 140) требует найти все возможные разбиения. Подход основан на возврате с возвратом и мемоизации: рекурсивно обрабатывайте каждую позицию и, если слово совпадает, рекурсивно обрабатывайте оставшуюся часть. Храните все частичные результаты как списки строк. Чтобы избежать TLE, запоминайте список предложений, возможных из каждой начальной позиции. В худшем случае количество предложений может быть экспоненциальным, но мемоизация устраняет повторные вычисления.

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

Оптимизация с помощью префиксного дерева

Когда словарь велик или слова длинные, проверка s[j:i] in word_set для всех j работает медленно из-за хеширования строк в Python. Префиксное дерево позволяет проходить по символам дерева, заранее отбрасывая невозможные пути. Вместо проверки всех O(n) начальных позиций Вы проходите только по существующим в дереве путям. Это значительно сокращает время работы на практике, если лишь немногие префиксы приводят к допустимым словам.

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

Граничные случаи и ограничения

Важные граничные случаи: (1) Пустая строка: верните True (пустая строка тривиально разбивается). (2) Слово отсутствует в словаре: DP никогда не установит соответствующую позицию в True и корректно вернёт False. (3) Перекрывающиеся слова: например, для слов «a» и «aa» в словаре и s='aaa' DP естественным образом обрабатывает этот случай, проверяя все значения j. (4) Повторяющиеся символы: для s='aaaaab' и dict=['a','aa','aaa'] число путей экспоненциально, но мемоизация ограничивает сложность значением 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)

Обобщение разбиения строк

Задача о разбиении строки на слова обобщается на любую задачу о разбиении строки: можно ли разделить строку s по некоторому правилу? Замените поиск в словаре любой проверкой за O(1) или O(L). Например, можно ли разбить s на палиндромы? Вместо множества слов используйте предварительно вычисленную таблицу палиндромов. Структура DP остаётся той же — изменяется только проверка допустимости.

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 и BFS

Задачу о разбиении строки на слова также можно представить как задачу поиска в ширину (BFS) кратчайшего пути: каждая позиция в строке является вершиной, а между j и i есть ребро, если s[j:i] содержится в словаре. Поиск в ширину из вершины 0 определяет, достижима ли вершина n. BFS имеет ту же сложность O(n² × L), но может быть более наглядным, если на собеседовании рассматривать задачу как графовую.

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

Стратегия объяснения на собеседовании

На собеседовании изложите ход рассуждений следующим образом: (1) Заметьте, что выбор на каждой позиции зависит от того, какие позиции были достижимы ранее, — это указывает на DP. (2) Определите состояние: dp[i] = можно ли разбить s[:i]? (3) До написания кода сформулируйте рекуррентное соотношение и базовый случай. (4) Сначала напишите решение O(n²), а затем упомяните оптимизацию с помощью префиксного дерева как возможное продолжение. (5) Обсудите граничные случаи: пустую строку, строку из одного символа и слово, отсутствующее в словаре.

# 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

Быстрая проверка

Проверьте, насколько Вы понимаете концепции курса «Структуры данных и алгоритмы — подготовка к собеседованию по программированию», рассмотренные в этом уроке.

Итоги урока

В этом уроке Вы узнали: dp[i] показывает, можно ли разбить s[:i] на слова из словаря, рекуррентное соотношение O(n²) проверяет все точки разделения j, для которых dp[j]=True и s[j:i] содержится в множестве слов и префиксное дерево может ускорить внутренний цикл, заранее отбрасывая несуществующие префиксы. Далее мы рассмотрим декодирование и подсчёт способов — ещё один одномерный шаблон DP, похожий на последовательность Фибоначчи.

Часто задаваемые вопросы

Урок «Разбиение слов и сегментация строки» бесплатный?

Да — полный текст урока «Разбиение слов и сегментация строки» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.

Чему я научусь в уроке «Разбиение слов и сегментация строки»?

Используйте одномерную таблицу DP, чтобы определить, можно ли разбить строку на слова из словаря, анализируйте сложность O(n²) и узнайте, почему бор ускоряет решение Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Coding Interview Prep?

Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.

Сколько времени занимает урок «Разбиение слов и сегментация строки»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Coding Interview Prep?

Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Грабитель домов: рекуррентное решение «взять или пропустить»
  2. Максимальный подмассив и подмассив с максимальным произведением
  3. Разбиение слов и сегментация строки
  4. Декодирование способов и подсчёт путей
← Назад к Coding Interview Prep