Problem of The Day: Word Search II
Problem Statement

Intuition
We can efficiently solve this problem by building a Trie from the list of words and using a backtracking algorithm to search the 2D board. The Trie allows us to quickly determine if the current character on the board can form part of a word in the dictionary. The backtracking ensures that we explore all potential words that can be formed by connecting adjacent characters.
Approach
- Construct a Trie from the list of words to efficiently check if a word or prefix exists.
- For each character in the board, use a backtracking algorithm to explore all possible paths, checking if they form a valid word.
- During backtracking, mark the board position as visited by temporarily replacing the character with a special symbol (e.g.,
##) to avoid revisiting it. - If a word is found during the search, add it to the result list.
- After exploring, reset the visited character to its original state.
Complexity
-
Time complexity: The time complexity is O(m \cdot n \cdot 4^l), where m \cdot n represents the size of the board, and l is the maximum length of the word in the dictionary. In the worst case, we may need to explore all four directions for each cell, leading to the factor of 4^l for backtracking.
-
Space complexity: The space complexity is O(l) due to the recursion depth during backtracking, where l is the length of the longest word. Additionally, building the Trie takes O(W \cdot L), where W is the number of words and L is the average length of the words.
Code
class Solution:
def backtrack(self, row, col, board, res, trie):
ROWS = len(board)
COLS = len(board[0])
if '#' in trie and trie['#'] not in res:
res.append(trie['#'])
temp = board[row][col]
board[row][col] = '#'
for x, y in [(1,0),(0,1),(-1,0),(0,-1)]:
r, c = row + x, col + y
if 0 <= r < ROWS and 0 <= c < COLS and board[r][c] in trie:
self.backtrack(r, c, board, res, trie[board[r][c]])
board[row][col] = temp
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
trie = {}
ROWS = len(board)
COLS = len(board[0])
res = []
for word in words:
root = trie
for c in word:
if c not in root:
root[c] = {}
root = root[c]
root['#'] = word
for row in range(ROWS):
for col in range(COLS):
if board[row][col] in trie:
self.backtrack(row, col, board, res, trie[board[row][col]])
return res
Editorial
Approach 1: Backtracking with Trie
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
WORD_KEY = "$"
trie = {}
for word in words:
node = trie
for letter in word:
# retrieve the next node; If not found, create a empty node.
node = node.setdefault(letter, {})
# mark the existence of a word in trie node
node[WORD_KEY] = word
rowNum = len(board)
colNum = len(board[0])
matchedWords = []
def backtracking(row, col, parent):
letter = board[row][col]
currNode = parent[letter]
# check if we find a match of word
word_match = currNode.pop(WORD_KEY, False)
if word_match:
# also we removed the matched word to avoid duplicates,
# as well as avoiding using set() for results.
matchedWords.append(word_match)
# Before the EXPLORATION, mark the cell as visited
board[row][col] = "#"
# Explore the neighbors in 4 directions, i.e. up, right, down, left
for rowOffset, colOffset in [(-1, 0), (0, 1), (1, 0), (0, -1)]:
newRow, newCol = row + rowOffset, col + colOffset
if (
newRow < 0
or newRow >= rowNum
or newCol < 0
or newCol >= colNum
):
continue
if not board[newRow][newCol] in currNode:
continue
backtracking(newRow, newCol, currNode)
# End of EXPLORATION, we restore the cell
board[row][col] = letter
# Optimization: incrementally remove the matched leaf node in Trie.
if not currNode:
parent.pop(letter)
for row in range(rowNum):
for col in range(colNum):
# starting from each of the cells
if board[row][col] in trie:
backtracking(row, col, trie)
return matchedWords

Leave a comment