Given a 2-D grid of characters board and a list of strings words, return all words that are present in the grid.
For a word to be present it must be possible to form the word with a path in the board with horizontally or vertically neighboring cells. The same cell may not be used more than once in a word.
Example 1:
Input:
board = [
["a","b","c","d"],
["s","a","a","t"],
["a","c","k","e"],
["a","c","d","n"]
],
words = ["bat","cat","back","backend","stack"]
Output: ["cat","back","backend"]Example 2:
Input:
board = [
["x","o"],
["x","o"]
],
words = ["xoxo"]
Output: []Constraints:
1 <= board.length, board[i].length <= 12board[i] consists only of lowercase English letter.1 <= words.length <= 30,0001 <= words[i].length <= 10words[i] consists only of lowercase English letters.words are distinct.
You should aim for a solution with O(m * n * 4 * (3^(t-1)) + s) time and O(s) space, where m is the number of rows, n is the number of columns, t is the maximum length of any word and s is the sum of the lengths of all the words.
To search for a word in the grid, we can use backtracking by starting at each cell, simultaneously iterating through the word and matching the characters with the cells, recursively visiting the neighboring cells. However, if we are given a list of words and need to search for each one, it becomes inefficient to iterate through each word and run backtracking for each. Can you think of a better way? Perhaps a data structure could help with more efficient word search and insertion.
We can use a Trie to efficiently search for multiple words. After inserting all words into the Trie, we traverse the grid once. For each character in the grid, we check if it exists in the current Trie node. If not, we prune the search. If we encounter an "end of word" flag in the Trie node, we know a valid word has been found. But how can we add that word to the result list? Maybe you should think about what additional information you can store in the Trie node.
When we insert a word into the Trie, we can store the word's index. Why? Because when we want to add the word to the result list after finding a valid word, we can easily add it using the index. After adding that word, we put index = -1 as we shouldn't add the word multiple times to the result list. How can you implement this?
We insert all the words into the Trie with their indices marked. Then, we iterate through each cell in the grid. At each cell, we start at the root of the Trie and explore all possible paths. As we traverse, we match characters in the cell with those in the Trie nodes. If we encounter the end of a word, we take the index at that node and add the corresponding word to the result list. Afterward, we unmark that index and continue exploring further paths.
Before attempting this problem, you should be comfortable with:
For each word, we try to trace it on the board by walking through adjacent cells (up/down/left/right).
To avoid using the same cell twice in one word path, we temporarily mark the cell as visited, and then restore it after exploring (classic backtracking).
If we can match all characters of a word in order, that word is found and added to the result.
ROWS, COLS be board dimensions.word in words:(r, c) as a possible starting point (only if it matches word[0]).DFS/backtracking function backtrack(r, c, i) where:i = current index in word we need to match.i == len(word), return true (whole word matched).word[i], return false.*).i + 1.word to the answer list and stop searching this word.class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
ROWS, COLS = len(board), len(board[0])
res = []
def backtrack(r, c, i):
if i == len(word):
return True
if (r < 0 or c < 0 or r >= ROWS or
c >= COLS or board[r][c] != word[i]
):
return False
board[r][c] = '*'
ret = (backtrack(r + 1, c, i + 1) or
backtrack(r - 1, c, i + 1) or
backtrack(r, c + 1, i + 1) or
backtrack(r, c - 1, i + 1))
board[r][c] = word[i]
return ret
for word in words:
flag = False
for r in range(ROWS):
if flag:
break
for c in range(COLS):
if board[r][c] != word[0]:
continue
if backtrack(r, c, 0):
res.append(word)
flag = True
break
return resWhere is the number of words, is the number of rows, is the number of columns and is the maximum length of any word in the array .
Searching each word separately repeats the same work many times.
A Trie (prefix tree) lets us share work: while walking on the board, we only continue paths that match some prefix of the given words.
So the board DFS explores "possible prefixes", and whenever the Trie node says this prefix is a complete word, we record it.
We also need to avoid reusing the same cell in a single path, so we keep a visited set during the current DFS path (and backtrack/remove when returning).
words.children (next letters) and isWord (true if a word ends here).res as a set (to avoid duplicates).visit as a set for the current DFS path.DFS dfs(r, c, node, wordSoFar):(r,c) is out of bounds, already visited, or board[r][c] is not in node.children, stop.(r,c) visited.node = node.children[board[r][c]]wordSoFar.node.isWord == true, add wordSoFar to res.node and wordSoFar.(r,c) from visit.DFS starting from every cell (r, c) with the Trie root.res.class TrieNode:
def __init__(self):
self.children = {}
self.isWord = False
def addWord(self, word):
cur = self
for c in word:
if c not in cur.children:
cur.children[c] = TrieNode()
cur = cur.children[c]
cur.isWord = True
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
root = TrieNode()
for w in words:
root.addWord(w)
ROWS, COLS = len(board), len(board[0])
res, visit = set(), set()
def dfs(r, c, node, word):
if (r < 0 or c < 0 or r >= ROWS or
c >= COLS or (r, c) in visit or
board[r][c] not in node.children
):
return
visit.add((r, c))
node = node.children[board[r][c]]
word += board[r][c]
if node.isWord:
res.add(word)
dfs(r + 1, c, node, word)
dfs(r - 1, c, node, word)
dfs(r, c + 1, node, word)
dfs(r, c - 1, node, word)
visit.remove((r, c))
for r in range(ROWS):
for c in range(COLS):
dfs(r, c, root, "")
return list(res)Where is the number of rows, is the number of columns, is the maximum length of any word in the array and is the sum of the lengths of all the words.
We still do DFS on the board, but we guide the DFS using a Trie so we only walk paths that match prefixes of the given words.
This version is faster because it adds aggressive pruning:
refs = "how many words in the dictionary still pass through this node".idx = -1 and decreasing refs).refs becomes 0, that branch is dead (no remaining words use it), so we physically cut the pointer from its parent (prev.children[...] = null).DFS calls from exploring useless prefixes.Also, instead of using a visited set, we mark the board in-place:
board[r][c] = '*' while exploring that path.Each node has:
children[26]: next letters (array, faster than hashmap)idx: index of a word in words if a word ends here, else -1refs: number of “still-alive” words that go through this node (including end words)Build the Trie:
words[i].refs on every node along the path.idx = i.DFS from every board cell:
dfs(r, c, node) tries to extend the current Trie path using board[r][c].DFS rules:
'*'), orboard[r][c]ch = board[r][c], move to child = node.children[ch]board[r][c] = '*'child.idx != -1, we found a word:words[child.idx] to resultchild.idx = -1 (avoid duplicates)child.refs (this word is removed from remaining dictionary)child.refs == 0, cut this branch from parent:node.children[ch] = nullchildReturn collected results.
class TrieNode:
def __init__(self):
self.children = [None] * 26
self.idx = -1
self.refs = 0
def addWord(self, word, i):
cur = self
cur.refs += 1
for c in word:
index = ord(c) - ord('a')
if not cur.children[index]:
cur.children[index] = TrieNode()
cur = cur.children[index]
cur.refs += 1
cur.idx = i
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
root = TrieNode()
for i in range(len(words)):
root.addWord(words[i], i)
ROWS, COLS = len(board), len(board[0])
res = []
def getIndex(c):
index = ord(c) - ord('a')
return index
def dfs(r, c, node):
if (r < 0 or c < 0 or r >= ROWS or
c >= COLS or board[r][c] == '*' or
not node.children[getIndex(board[r][c])]):
return
tmp = board[r][c]
board[r][c] = '*'
prev = node
node = node.children[getIndex(tmp)]
if node.idx != -1:
res.append(words[node.idx])
node.idx = -1
node.refs -= 1
if not node.refs:
prev.children[getIndex(tmp)] = None
node = None
board[r][c] = tmp
return
dfs(r + 1, c, node)
dfs(r - 1, c, node)
dfs(r, c + 1, node)
dfs(r, c - 1, node)
board[r][c] = tmp
for r in range(ROWS):
for c in range(COLS):
dfs(r, c, root)
return resWhere is the number of rows, is the number of columns, is the maximum length of any word in the array and is the sum of the lengths of all the words.
When multiple paths on the board can form the same word, adding the word to the result list without checking for duplicates leads to duplicate entries. Using a set for results or marking words as found in the Trie (by setting idx = -1 or isWord = false) prevents this issue.
When marking cells as visited by modifying the board directly (e.g., setting board[r][c] = '*'), forgetting to restore the original character after the recursive calls will corrupt the board state. This prevents other starting positions or words from finding valid paths.
Building a Trie but not pruning it after finding words leads to repeated exploration of dead branches. Without decrementing refs counts and removing nodes when refs reaches zero, the algorithm continues searching paths that cannot yield any new words, significantly degrading performance.
Advancing the Trie pointer before checking if the child node exists can cause null pointer exceptions. Always verify that node.children[char] exists before moving to the next node. Similarly, checking isWord before updating the node pointer will miss the current word.
When using a separate visited set alongside the Trie, forgetting to clear or properly backtrack the visited set for each new starting cell can block valid paths. Each DFS starting point should begin with a fresh or properly reset visited state.