Word Search II: Trie i przeszukiwanie z nawrotami na siatce
Wstawiać wszystkie docelowe słowa do trie i uruchamiać przeszukiwanie DFS z nawrotami na planszy 2D, aby jednocześnie znaleźć wszystkie poprawne słowa w czasie O(m × n × 4^L)
Word Search II: Trie i przeszukiwanie z nawrotami na siatce to bezpłatna lekcja DSA Interview Prep na CoddyKit. To lekcja 4 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 Search II
Word Search II (LeetCode 212): dla planszy znaków o wymiarach m × n i listy słów należy znaleźć wszystkie słowa, które można utworzyć z kolejno sąsiadujących komórek (poziomo lub pionowo), przy czym każdej komórki można użyć tylko raz. Jest to trudniejsze niż Word Search I (jedno słowo), ponieważ trzeba znaleźć wszystkie pasujące słowa jednocześnie — naiwne uruchamianie Word Search I dla każdego słowa ma złożoność O(W × m × n × 4^L), co jest zbyt wolne.
Dlaczego trie i backtracking?
Wstawienie wszystkich docelowych słów do trie, a następnie uruchomienie wyszukiwania DFS z nawrotami na planszy pozwala wyszukiwać wszystkie słowa jednocześnie. Dla każdej komórki planszy zamiast sprawdzać „czy ta ścieżka tworzy moje docelowe słowo?”, sprawdzamy „czy ta ścieżka odpowiada prefiksowi w trie?”. Gdy tylko prefiks w trie przestaje pasować, przycinamy całą gałąź DFS — unikając powtarzania pracy dla wszystkich słów współdzielących ten prefiks.
Budowanie trie z listy słów
Wstawiamy wszystkie słowa do trie. Pełne słowo przechowujemy w węźle liścia (w node.word), a nie tylko wartość logiczną, aby po znalezieniu pełnego dopasowania podczas backtrackingu natychmiast dodać słowo do wyników bez odtwarzania go znak po znaku.
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 z nawrotami na planszy
Uruchamiamy DFS z każdej komórki planszy. Na każdym kroku: (1) sprawdzamy, czy znak bieżącej komórki istnieje jako dziecko bieżącego węzła trie; (2) jeśli tak, oznaczamy komórkę jako odwiedzoną (ustawiamy w niej znacznik, taki jak '#') i wywołujemy rekurencję dla 4 sąsiadów; (3) po zakończeniu rekurencji przywracamy komórkę (usuwamy oznaczenie). Gdy węzeł trie ma niepustą wartość word, dodajemy ją do wyników i ustawiamy ją na None, aby uniknąć duplikatów.
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']Analiza złożoności
Czas: O(m × n × 4^L), gdzie L to maksymalna długość słowa. Dla każdej z m×n komórek początkowych DFS przechodzi maksymalnie 4^L ścieżek. Trie przycina ścieżki, które nie odpowiadają żadnemu prefiksowi słowa, dlatego w praktyce rozwiązanie jest znacznie szybsze. Zbudowanie trie ma złożoność O(W × L), gdzie W to liczba słów. Pamięć: O(W × L) na trie oraz O(L) na głębokość stosu rekurencji.
Przycinanie: usuwanie liści po znalezieniu słowa
Po znalezieniu słowa należy usunąć węzeł liścia z trie (a nie tylko wyzerować słowo), jeśli nie ma on dzieci. Zapobiega to ponownemu odwiedzaniu martwych gałęzi w kolejnych wywołaniach DFS. Gdy po znalezieniu słowa dzieci danego węzła zostaną usunięte, usuwamy ten węzeł ze słownika dzieci jego rodzica. Ta optymalizacja ma duże znaczenie, gdy wiele słów współdzieli długie prefiksy.
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')Dlaczego przechowywanie word w węźle jest lepsze
Przechowywanie pełnego słowa w liściu trie (zamiast odtwarzania go na podstawie ścieżki DFS) ma dwie zalety: (1) pobranie słowa po znalezieniu dopasowania zajmuje O(1), zamiast O(L) potrzebnego na odtworzenie ścieżki; (2) ustawienie node.word = None po znalezieniu słowa zapewnia proste usuwanie duplikatów w czasie O(1), bez potrzeby korzystania z osobnego zbioru wyników. W szczególności w Word Search II zapobieganie duplikatom jest istotne, ponieważ to samo słowo może teoretycznie zostać znalezione różnymi ścieżkami.
Oznaczanie odwiedzonych komórek w miejscu
Zamiast osobnego zbioru visited (który wymagałby O(m × n) pamięci dla każdej ścieżki DFS) oznaczamy komórki w miejscu, zastępując ich znak znacznikiem, takim jak '#'. Po powrocie z DFS przywracamy oryginalny znak. Technika ta: (1) używa O(1) dodatkowej pamięci na komórkę; (2) automatycznie zapobiega ponownemu odwiedzeniu komórki w ramach jednej ścieżki; (3) jest całkowicie niewidoczna dla przechodzenia po trie, ponieważ '#' nigdy nie wystąpi w trie.
Przypadki brzegowe do obsłużenia
Ważne przypadki brzegowe: (1) zduplikowane słowa na liście słów — należy przechowywać je w zbiorze albo użyć sztuczki node.word = None, aby zapobiec duplikatom w wynikach; (2) bardzo długie słowa przekraczające wymiary planszy — nie można ich utworzyć, ale DFS naturalnie sobie z tym radzi, gdy zabraknie sąsiednich komórek; (3) plansza jednokomórkowa — można znaleźć tylko słowa składające się z jednego znaku; (4) to samo słowo możliwe do znalezienia różnymi ścieżkami — sztuczka node.word = None zapobiega wielokrotnemu zliczaniu.
Porównanie z podejściem naiwnym
Podejście naiwne: dla każdego z W słów uruchamiamy Word Search I, uzyskując O(W × m × n × 4^L). W przypadku trie wszystkie słowa są wyszukiwane jednocześnie: O(m × n × 4^L), niezależnie od W. Dla W=1000 słów o długości 10 na planszy 10×10 podejście naiwne jest 1000 razy wolniejsze niż rozwiązanie z trie. Trie działa jak współdzielony filtr prefiksów, który rozkłada koszt na wszystkie słowa — jest to klasyczny przykład wykorzystania struktury danych do uzyskania poprawy asymptotycznej.
Podsumowanie pełnego rozwiązania
Kompletne rozwiązanie Word Search II: budujemy trie ze słów i przechowujemy ciąg znaków słowa w liściu. Dla każdej komórki planszy uruchamiamy DFS: sprawdzamy, czy bieżący znak istnieje w bieżącym węźle trie, oznaczamy komórkę jako '#', wywołujemy rekurencję dla 4 sąsiadów i przywracamy komórkę. Gdy node.word nie ma wartości null, dodajemy słowo do wyników i ustawiamy je na null. Opcjonalnie po użyciu przycinamy puste gałęzie trie. Zwracamy listę wyników. Czas: O(m×n×4^L), pamięć: O(W×L) na trie + O(L) na rekurencję.
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 resultSzybki test
Sprawdź swoje rozumienie zagadnień Data Structures & Algorithms — Coding Interview Prep z tej lekcji.
Podsumowanie lekcji
W tej lekcji omówiono: Word Search II wykorzystuje trie do jednoczesnego wyszukiwania wielu słów ze współdzielonym przycinaniem prefiksów, przechowywanie ciągu znaków słowa w liściu trie umożliwia pobranie słowa w czasie O(1) oraz łatwe usuwanie duplikatów przez ustawienie go na None po znalezieniu oraz oznaczanie odwiedzonych komórek w miejscu eliminuje potrzebę użycia O(m×n) dodatkowej pamięci dla każdej ścieżki DFS. W ten sposób kończy się kurs Tries and String Algorithms — opanowano jedną z najpotężniejszych struktur danych przeznaczonych do pracy z napisami, wykorzystywanych podczas rozmów technicznych.
Ucz się Python dzięki korepetycjom AI — za darmo
Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.
- Kursy
- 30
- Lekcje
- 120
Często zadawane pytania
Czy lekcja „Word Search II: Trie i przeszukiwanie z nawrotami na siatce” jest bezpłatna?
Tak — pełny tekst „Word Search II: Trie i przeszukiwanie z nawrotami na siatce” 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 Search II: Trie i przeszukiwanie z nawrotami na siatce”?
Wstawiać wszystkie docelowe słowa do trie i uruchamiać przeszukiwanie DFS z nawrotami na planszy 2D, aby jednocześnie znaleźć wszystkie poprawne słowa w czasie O(m × n × 4^L) Ć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 4 z 4.
Ile czasu zajmuje lekcja „Word Search II: Trie i przeszukiwanie z nawrotami na siatce”?
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
- Klasa TrieNode: wstawianie i wyszukiwanie
- Wyszukiwanie prefiksów i Starts-With
- Wyszukiwanie z symbolami wieloznacznymi i wyrażeniami regularnymi w Trie
- Word Search II: Trie i przeszukiwanie z nawrotami na siatce