Word Search II hard
Problem Statement
Given an m x n
board of characters and a list of strings words
, return all words on the board.
Each word must be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically connected. The same letter cell may not be used more than once in a word.
Example 1
Input: board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"] Output: ["eat","oath"]
Example 2
Input: board = [["a","b"],["c","d"]], words = ["abcb"] Output: []
Steps
-
Trie Data Structure: Build a Trie (prefix tree) from the list of words. A Trie efficiently stores and searches for words based on prefixes. Each node in the Trie represents a character, and paths from the root to leaf nodes represent words.
-
Depth-First Search (DFS): Perform a DFS on the board. For each cell, explore all possible paths starting from that cell. As we explore a path, we concurrently traverse the Trie.
-
Trie Traversal and Word Discovery: If a path's character sequence matches a prefix in the Trie, we continue the DFS. If a path's character sequence reaches a leaf node in the Trie, we've found a word, add it to the result set (ensuring we don't add duplicates).
-
Backtracking: After exploring a path, backtrack to the previous cell to explore other paths from that cell. This ensures we don't reuse cells within a word.
-
Optimization: Mark visited cells during DFS to avoid cycles and redundant searches.
Explanation
The Trie significantly optimizes the search. Instead of searching for each word individually, we perform one comprehensive search using the Trie to check for prefixes. DFS efficiently explores all possible word formations on the board, and backtracking guarantees we examine all possibilities without repetition.
Code
class TrieNode:
def __init__(self):
self.children = {}
self.is_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word):
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_word = True
def search(self, word):
node = self.root
for char in word:
if char not in node.children:
return False
node = node.children[char]
return node.is_word
def findWords(board, words):
trie = Trie()
for word in words:
trie.insert(word)
m, n = len(board), len(board[0])
result = set()
def dfs(row, col, node, word):
if row < 0 or row >= m or col < 0 or col >= n or (row, col) in visited:
return
char = board[row][col]
if char not in node.children:
return
visited.add((row, col))
node = node.children[char]
word += char
if node.is_word:
result.add(word)
dfs(row + 1, col, node, word)
dfs(row - 1, col, node, word)
dfs(row, col + 1, node, word)
dfs(row, col - 1, node, word)
visited.remove((row, col))
visited = set()
for r in range(m):
for c in range(n):
dfs(r, c, trie.root, "")
return list(result)
Complexity
- Time Complexity: O(M * N * 4^L), where M and N are the dimensions of the board, and L is the maximum length of a word. In the worst case, we explore all possible paths on the board.
- Space Complexity: O(M * N + sum of word lengths). This accounts for the space used by the board, visited set, and Trie. The Trie's space is proportional to the total length of all words.