Поиск слов II: дерево префиксов и перебор с возвратом на сетке
Вставьте все целевые слова в дерево префиксов и выполните DFS с возвратом на двумерной доске, чтобы одновременно найти все подходящие слова за O(m × n × 4^L)
«Поиск слов II: дерево префиксов и перебор с возвратом на сетке» — бесплатный урок DSA Interview Prep на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DSA Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DSA Interview Prep содержит 4 уроков всего.
Задача «Поиск слов II»
«Поиск слов II» (LeetCode 212): дана доска символов размера m × n и список слов; необходимо найти все слова, которые можно составить из последовательно соседних клеток (по горизонтали или вертикали), причём каждую клетку можно использовать только один раз. Это сложнее, чем «Поиск слов I» (одно слово), поскольку необходимо одновременно найти все совпадающие слова — прямой запуск «Поиска слов I» для каждого слова имеет сложность O(W × m × n × 4^L), что слишком медленно.
Зачем нужны префиксное дерево и поиск с возвратом
Если вставить все целевые слова в префиксное дерево, а затем запустить поиск с возвратом на доске, можно искать все слова одновременно. Для каждой клетки доски вместо проверки «составляет ли этот путь моё целевое слово?» проверяется «соответствует ли этот путь префиксу в префиксном дереве?». Как только префикс в префиксном дереве перестаёт соответствовать, вся ветвь поиска с возвратом отсекается — это позволяет избежать повторной работы для всех слов с общим префиксом.
Построение префиксного дерева по списку слов
Вставьте все слова в префиксное дерево. Храните полное слово в конечном узле (в node.word), а не только логическое значение, чтобы при обнаружении полного совпадения во время поиска с возвратом можно было сразу добавить слово в результаты, не восстанавливая его посимвольно.
class TrieNode:
def __init__(self):
self.children = {}
self.word = None # stores the complete word if this is an end node
def build_trie(words):
root = TrieNode()
for word in words:
node = root
for c in word:
if c not in node.children:
node.children[c] = TrieNode()
node = node.children[c]
node.word = word # mark complete word here
return root
root = build_trie(['eat','oath','ot'])
print('Trie built with', len(root.children), 'root children')Поиск с возвратом на сетке с помощью DFS
Запустите DFS из каждой клетки доски. На каждом шаге: (1) проверьте, существует ли символ текущей клетки среди дочерних узлов текущего узла префиксного дерева; (2) если да, отметьте клетку как посещённую (замените её специальным маркером, например '#') и рекурсивно обработайте 4 соседние клетки; (3) после рекурсии восстановите клетку (снимите отметку). Если у узла префиксного дерева значение word не равно None, добавьте его в результаты и присвойте ему None, чтобы избежать повторов (duplicates).
class TrieNode:
def __init__(self):
self.children = {}
self.word = None
def findWords(board, words):
root = TrieNode()
for word in words:
node = root
for c in word:
if c not in node.children:
node.children[c] = TrieNode()
node = node.children[c]
node.word = word
m, n = len(board), len(board[0])
result = []
def dfs(i, j, node):
c = board[i][j]
if c not in node.children:
return
next_node = node.children[c]
if next_node.word:
result.append(next_node.word)
next_node.word = None # avoid duplicates
board[i][j] = '#' # mark visited
for di, dj in [(-1,0),(1,0),(0,-1),(0,1)]:
ni, nj = i+di, j+dj
if 0<=ni<m and 0<=nj<n and board[ni][nj] != '#':
dfs(ni, nj, next_node)
board[i][j] = c # restore
for i in range(m):
for j in range(n):
dfs(i, j, root)
return result
board = [['o','a','a','n'],['e','t','a','e'],['i','h','k','r'],['i','f','l','v']]
words = ['oath','pea','eat','rain']
print(findWords(board, words)) # ['oath','eat']Анализ сложности
Время: O(m × n × 4^L), где L — максимальная длина слова. Для каждой из m×n начальных клеток DFS исследует до 4^L путей. Префиксное дерево отсекает пути, не соответствующие ни одному префиксу слова, поэтому на практике алгоритм работает намного быстрее. Построение префиксного дерева выполняется за O(W × L), где W — количество слов. Память: O(W × L) для префиксного дерева плюс O(L) для глубины стека рекурсии.
Отсечение: удаление листовых узлов после нахождения слова
После нахождения слова удалите листовой узел из префиксного дерева (а не только обнулите слово), если у него нет дочерних узлов. Это предотвращает повторное посещение бесполезных ветвей в последующих вызовах DFS. Когда после нахождения слова дочерние узлы некоторого узла становятся недоступны, удалите этот узел из словаря дочерних узлов его родителя. Такая оптимизация особенно важна, когда многие слова имеют длинные общие префиксы.
def dfs_with_pruning(i, j, node, board, m, n, result):
c = board[i][j]
if c not in node.children:
return
next_node = node.children[c]
if next_node.word:
result.append(next_node.word)
next_node.word = None
board[i][j] = '#'
for di, dj in [(-1,0),(1,0),(0,-1),(0,1)]:
ni, nj = i+di, j+dj
if 0<=ni<m and 0<=nj<n and board[ni][nj] != '#':
dfs_with_pruning(ni, nj, next_node, board, m, n, result)
board[i][j] = c
# Prune: if the node has no more children and no word, remove it
if not next_node.children and not next_node.word:
del node.children[c]
print('Leaf pruning removes exhausted trie branches during search')Почему лучше хранить word в узле
Хранение полного слова в листовом узле префиксного дерева (вместо его восстановления по пути DFS) даёт два преимущества: (1) получение слова за O(1) при обнаружении совпадения вместо восстановления пути за O(L); (2) присваивание node.word = None после нахождения слова обеспечивает простое устранение повторов за O(1) без отдельного множества результатов. В частности, для «Поиска слов II» предотвращение повторов важно, поскольку одно и то же слово теоретически может быть найдено по разным путям.
Пометка посещённых клеток непосредственно на доске
Вместо отдельного множества visited (для которого потребовалось бы O(m × n) памяти на каждый путь DFS) помечайте клетки непосредственно на доске, заменяя их символ специальным маркером, например '#'. После возврата из DFS восстановите исходный символ. Этот приём: (1) использует O(1) дополнительной памяти на клетку; (2) автоматически предотвращает повторное посещение в рамках одного пути; (3) полностью прозрачен для обхода префиксного дерева, поскольку '#' никогда не встречается в нём.
Граничные случаи
Важные граничные случаи: (1) повторяющиеся слова в списке слов — сохраните слова в множестве или используйте приём node.word = None, чтобы предотвратить повторы в результатах; (2) очень длинные слова, превышающие размеры доски, — их нельзя составить, но DFS естественным образом обрабатывает этот случай, когда заканчиваются соседние клетки; (3) доска из одной клетки — можно найти только слова из одного символа; (4) одно и то же слово, которое можно найти по разным путям, — приём node.word = None предотвращает повторный подсчёт.
Сравнение с наивным подходом
Наивный подход: для каждого из W слов запускать «Поиск слов I»: O(W × m × n × 4^L). С префиксным деревом все слова ищутся одновременно: O(m × n × 4^L) независимо от W. Для W=1000 слов длиной 10 на доске 10×10 наивный подход в 1000 раз медленнее префиксного дерева. Префиксное дерево действует как общий фильтр префиксов, распределяя затраты между всеми словами, — это классический пример использования структуры данных для достижения асимптотического улучшения.
Итоговое описание решения
Полное решение задачи «Поиск слов II»: постройте префиксное дерево по словам и храните строку слова в листе. Для каждой клетки доски запустите DFS: проверьте, существует ли текущий символ в текущем узле префиксного дерева, пометьте клетку символом '#', рекурсивно обработайте 4 соседние клетки и восстановите клетку. Если значение node.word не равно null, добавьте его в результаты и обнулите. При необходимости удаляйте пустые ветви префиксного дерева после использования. Верните список результатов. Время: O(m×n×4^L), память: O(W×L) для префиксного дерева + O(L) для рекурсии.
class TrieNode:
def __init__(self):
self.children = {}
self.word = None
def findWords_final(board, words):
root = TrieNode()
for word in words:
node = root
for c in word:
node = node.children.setdefault(c, TrieNode())
node.word = word
m, n = len(board), len(board[0])
result = []
def dfs(i, j, node):
c = board[i][j]
child = node.children.get(c)
if not child:
return
if child.word:
result.append(child.word)
child.word = None
board[i][j] = '#'
for di, dj in [(-1,0),(1,0),(0,-1),(0,1)]:
ni, nj = i+di, j+dj
if 0<=ni<m and 0<=nj<n and board[ni][nj] != '#':
dfs(ni, nj, child)
board[i][j] = c
if not child.children:
del node.children[c]
for i in range(m):
for j in range(n):
dfs(i, j, root)
return resultБыстрая проверка
Проверьте своё понимание концепций курса «Структуры данных и алгоритмы — подготовка к техническому собеседованию», рассмотренных в этом уроке.
Итоги урока
В этом уроке вы изучили следующее: «Поиск слов II» использует префиксное дерево для одновременного поиска нескольких слов с отсечением общих префиксов, хранение строки слова в листе префиксного дерева обеспечивает получение слова за O(1) и простое устранение повторов с помощью обнуления значения после нахождения, а также пометка посещённых клеток непосредственно на доске с помощью '#' позволяет избежать O(m×n) дополнительной памяти для каждого пути DFS. На этом курс «Префиксные деревья и алгоритмы работы со строками» завершён — вы освоили одну из самых мощных структур данных для работы со строками, используемых на собеседованиях.
Часто задаваемые вопросы
Урок «Поиск слов II: дерево префиксов и перебор с возвратом на сетке» бесплатный?
Да — полный текст урока «Поиск слов II: дерево префиксов и перебор с возвратом на сетке» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс DSA Interview Prep, подпишись на CoddyKit PRO. Курс DSA Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Поиск слов II: дерево префиксов и перебор с возвратом на сетке»?
Вставьте все целевые слова в дерево префиксов и выполните DFS с возвратом на двумерной доске, чтобы одновременно найти все подходящие слова за O(m × n × 4^L) Ты практикуешь DSA Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать DSA Interview Prep?
Предыдущий опыт не требуется. DSA Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Поиск слов II: дерево префиксов и перебор с возвратом на сетке»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке DSA Interview Prep?
Да. Каждый урок DSA Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Класс TrieNode: вставка и поиск
- Поиск по префиксу и проверка начала строки
- Поиск по шаблону и регулярному выражению в дереве префиксов
- Поиск слов II: дерево префиксов и перебор с возвратом на сетке