Разбиение слов и сегментация строки
Используйте одномерную таблицу 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 — локальная установка не требуется.
Все уроки этого курса
- Грабитель домов: рекуррентное решение «взять или пропустить»
- Максимальный подмассив и подмассив с максимальным произведением
- Разбиение слов и сегментация строки
- Декодирование способов и подсчёт путей