Skip to content

Commit 81cdecd

Browse files
committed
solve(w14): 212. Word Search II
1 parent cc007f4 commit 81cdecd

File tree

1 file changed

+180
-0
lines changed

1 file changed

+180
-0
lines changed

word-search-ii/seungriyou.py

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# https://leetcode.com/problems/word-search-ii/
2+
3+
from typing import List
4+
5+
"""#### [Approach 1] ####
6+
[Complexity]
7+
(k = len(words), l = max(len(word) for word in words))
8+
- TC: O(m * n * 4 * 3^(l-1))
9+
- trie 구성: O(k * l) = O(total # of letters in words)
10+
- backtracking: 이론적으로 O(m * n * (4 * 3^(l-1)))
11+
- SC: O(k * l) (고정)
12+
- trie 저장: O(k * l) = O(total # of letters in words)
13+
(word도 저장하므로 2배)
14+
- call stack: O(l)
15+
16+
[Approach]
17+
주어진 words에 대해 기본 trie를 구성하고,
18+
board 내의 문자들을 순회하며 해당 문자로 시작하는 단어가 trie에 있는지 backtracking으로 확인한다.
19+
20+
이때, words = ["oa", "oaa"]인 경우에 res = ["oa", "oa", "oaa"]가 나오지 않도록 하기 위해서
21+
word를 찾은 경우 node.word = None으로 중복을 제거하는 로직이 필요하다. (혹은 res를 set()으로 기록)
22+
"""
23+
24+
from collections import defaultdict
25+
26+
class TrieNode:
27+
def __init__(self):
28+
self.children = defaultdict(TrieNode)
29+
self.word = None
30+
31+
class Trie:
32+
def __init__(self):
33+
self.root = TrieNode()
34+
35+
def insert(self, word):
36+
node = self.root
37+
for w in word:
38+
node = node.children[w]
39+
node.word = word
40+
41+
class Solution:
42+
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
43+
m, n = len(board), len(board[0])
44+
res = []
45+
46+
# 1. trie 구성
47+
trie = Trie()
48+
for word in words:
49+
trie.insert(word)
50+
51+
# 2. board 내 문자를 순회하며 trie에서 찾기 (w. backtracking)
52+
def backtrack(r, c, node):
53+
# base condition
54+
if (
55+
not (0 <= r < m and 0 <= c < n) # (1) 범위를 벗어났거나
56+
or board[r][c] == "#" # (2) 이미 방문했거나
57+
or board[r][c] not in node.children # (3) 현재 node의 children에 문자가 없다면
58+
):
59+
return
60+
61+
# visit 처리
62+
curr_w, board[r][c] = board[r][c], "#"
63+
64+
# trie 타고 내려가기
65+
node = node.children[curr_w]
66+
67+
# 내려간 node가 word 라면 결과에 추가
68+
if node.word:
69+
res.append(node.word)
70+
node.word = None # -- 중복 제거
71+
72+
# recur
73+
backtrack(r - 1, c, node)
74+
backtrack(r + 1, c, node)
75+
backtrack(r, c - 1, node)
76+
backtrack(r, c + 1, node)
77+
78+
# visit 처리 취소
79+
board[r][c] = curr_w
80+
81+
for i in range(m):
82+
for j in range(n):
83+
backtrack(i, j, trie.root)
84+
85+
return res
86+
87+
88+
"""#### [Approach 2] ####
89+
[Complexity]
90+
(k = len(words), l = max(len(word) for word in words))
91+
- TC: O(m * n * 4 * 3^(l-1))
92+
- trie 구성: O(k * l) = O(total # of letters in words)
93+
- backtracking: 이론적으로 O(m * n * (4 * 3^(l-1)))이나,
94+
탐색 공간이 remove로 줄어들기 때문에 (4 * 3^(l-1)) 실제로는 경로가 빠르게 줄어듦
95+
- remove: O(k * l) (k개의 단어에 대해서)
96+
- SC: O(k * l) (점점 줄어듦)
97+
- trie 저장: O(k * l) = O(total # of letters in words)
98+
(word도 저장하므로 2배) (실행 중에 줄어듦)
99+
- call stack: O(l)
100+
101+
[Approach]
102+
approach 1과 비교했을 때, trie.remove(node)를 통해 이미 찾은 단어의 경로를 trie에서 제거한다는 점이 다르다.
103+
따라서 이미 찾은 word의 경로는 더 이상 탐색하지 않으며, trie의 크기가 점차 줄어들어 탐색 공간도 줄어들게 된다.
104+
(TrieNode에 parent, char 추가)
105+
"""
106+
107+
class TrieNode:
108+
def __init__(self, parent=None, char=None):
109+
self.children = {}
110+
self.word = None
111+
self.parent = parent # for optimization
112+
self.char = char # for optimization
113+
114+
class Trie:
115+
def __init__(self):
116+
self.root = TrieNode()
117+
118+
def insert(self, word):
119+
node = self.root
120+
for w in word:
121+
if w not in node.children: # node.children에 w가 없을 때만 새로 만들기
122+
node.children[w] = TrieNode(parent=node, char=w)
123+
node = node.children[w]
124+
node.word = word
125+
126+
def remove(self, node): # for optimization (이미 찾은 단어는 trie에서 없애기)
127+
# 중복 제거
128+
node.word = None
129+
130+
# trie에서 현재 word를 제거 (단, child.children이 없어야 제거 가능)
131+
child, parent = node, node.parent
132+
while parent and len(child.children) == 0:
133+
del parent.children[child.char]
134+
child, parent = parent, parent.parent
135+
136+
class Solution:
137+
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
138+
m, n = len(board), len(board[0])
139+
res = []
140+
141+
# 1. trie 구성
142+
trie = Trie()
143+
for word in words:
144+
trie.insert(word)
145+
146+
# 2. board 내 문자를 순회하며 trie에서 찾기 (w. backtracking)
147+
def backtrack(r, c, node):
148+
# base condition
149+
if (
150+
not (0 <= r < m and 0 <= c < n) # (1) 범위를 벗어났거나
151+
or board[r][c] == "#" # (2) 이미 방문했거나
152+
or board[r][c] not in node.children # (3) 현재 node의 children에 문자가 없다면
153+
):
154+
return
155+
156+
# visit 처리
157+
curr_w, board[r][c] = board[r][c], "#"
158+
159+
# trie 타고 내려가기
160+
node = node.children[curr_w]
161+
162+
# 내려간 node가 word 라면 결과에 추가
163+
if node.word:
164+
res.append(node.word)
165+
trie.remove(node)
166+
167+
# recur
168+
backtrack(r - 1, c, node)
169+
backtrack(r + 1, c, node)
170+
backtrack(r, c - 1, node)
171+
backtrack(r, c + 1, node)
172+
173+
# visit 처리 취소
174+
board[r][c] = curr_w
175+
176+
for i in range(m):
177+
for j in range(n):
178+
backtrack(i, j, trie.root)
179+
180+
return res

0 commit comments

Comments
 (0)