Word Search II (Trie + Backtrack)
Imagine you have a large grid of letters and a dictionary of words. You want to find all the words from the dictionary that can be formed by tracing adjacent letters in the grid, like a word puzzle solver.
Given a 2D board of characters and a list of words, find all words on the board. Each word must be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once in a word.
1 <= board.length, board[i].length <= 121 <= words.length <= 3 * 10^41 <= words[i].length <= 10board and words consist of lowercase English letters"board = [['o','a','a','n'],['e','t','a','e'],['i','h','k','r'],['i','f','l','v']], words = ['oath','pea','eat','rain']"['eat','oath']The words 'eat' and 'oath' can be formed by adjacent letters on the board, 'pea' and 'rain' cannot.
- Empty board → returns empty list
- Words list empty → returns empty list
- Words with repeated letters requiring careful backtracking → correct detection
- Board with all identical letters and multiple words → finds all valid words
Leads to revisiting same cell multiple times causing incorrect words or infinite loops
✅ Use a visited array or in-place marking and restore after backtracking
Causes redundant searches and slower runtime
✅ Remove Trie nodes with no children after word found to prune search space
Leads to TLE on large inputs
✅ Build a Trie for all words and backtrack once to find all matches
Same word appears multiple times in result
✅ Mark found words as null in Trie to avoid duplicates
Subsequent searches fail or produce wrong results
✅ Restore original letter after recursive calls
Intuition
Try to find each word on the board by starting from every cell and exploring all paths recursively.
Algorithm
- For each word in the list:
- For each cell in the board:
- Use DFS to check if the word can be formed starting from that cell.
- If found, add the word to the result list.
from typing import List
def exist(board: List[List[str]], word: str) -> bool:
rows, cols = len(board), len(board[0])
visited = [[False]*cols for _ in range(rows)]
def dfs(r, c, idx):
if idx == len(word):
return True
if r < 0 or r >= rows or c < 0 or c >= cols or visited[r][c] or board[r][c] != word[idx]:
return False
visited[r][c] = True
res = (dfs(r+1, c, idx+1) or dfs(r-1, c, idx+1) or dfs(r, c+1, idx+1) or dfs(r, c-1, idx+1))
visited[r][c] = False
return res
for r in range(rows):
for c in range(cols):
if dfs(r, c, 0):
return True
return False
def findWords(board: List[List[str]], words: List[str]) -> List[str]:
result = []
for word in words:
if exist(board, word):
result.append(word)
return result
# Driver code
if __name__ == '__main__':
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))def exist(board: List[List[str]], word: str) -> bool:Defines a helper function to check if a single word exists on the boardvisited = [[False]*cols for _ in range(rows)]Tracks visited cells to avoid reusing letters in the same word pathif idx == len(word):Base case: all characters matched successfullyvisited[r][c] = TrueMark current cell as visited before exploring neighborsvisited[r][c] = FalseBacktrack by unmarking the cell after exploringfor r in range(rows):Try starting DFS from every cell in the boardif dfs(r, c, 0):If DFS finds the word starting at this cell, return Truefor word in words:Check each word independently, leading to high time complexityimport java.util.*;
public class WordSearchBrute {
private static boolean exist(char[][] board, String word) {
int rows = board.length, cols = board[0].length;
// For each cell, try DFS with fresh visited array
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
boolean[][] visited = new boolean[rows][cols];
if (dfs(board, word, r, c, 0, visited)) {
return true;
}
}
}
return false;
}
private static boolean dfs(char[][] board, String word, int r, int c, int idx, boolean[][] visited) {
if (idx == word.length()) return true;
if (r < 0 || r >= board.length || c < 0 || c >= board[0].length || visited[r][c] || board[r][c] != word.charAt(idx))
return false;
visited[r][c] = true;
boolean res = dfs(board, word, r + 1, c, idx + 1, visited) ||
dfs(board, word, r - 1, c, idx + 1, visited) ||
dfs(board, word, r, c + 1, idx + 1, visited) ||
dfs(board, word, r, c - 1, idx + 1, visited);
visited[r][c] = false;
return res;
}
public static List<String> findWords(char[][] board, String[] words) {
List<String> result = new ArrayList<>();
for (String word : words) {
if (exist(board, word)) {
result.add(word);
}
}
return result;
}
public static void main(String[] args) {
char[][] board = {
{'o','a','a','n'},
{'e','t','a','e'},
{'i','h','k','r'},
{'i','f','l','v'}
};
String[] words = {"oath", "pea", "eat", "rain"};
System.out.println(findWords(board, words));
}
}private static boolean exist(char[][] board, String word)Checks if a single word exists on the board by trying DFS from every cell with fresh visited arrayfor (int r = 0; r < rows; r++)Iterate over all rows as starting pointsfor (int c = 0; c < cols; c++)Iterate over all columns as starting pointsboolean[][] visited = new boolean[rows][cols];Create a fresh visited array for each DFS to avoid contaminationif (dfs(board, word, r, c, 0, visited))If DFS finds the word starting at this cell, return trueprivate static boolean dfs(char[][] board, String word, int r, int c, int idx, boolean[][] visited)DFS helper to check if word exists starting at (r,c)if (idx == word.length()) return true;Base case: all characters matchedvisited[r][c] = true;Mark cell visited before recursive callsvisited[r][c] = false;Backtrack by unmarking after recursionfor (String word : words)Check each word independently, leading to high time complexity#include <iostream>
#include <vector>
#include <string>
#include <unordered_set>
using namespace std;
bool dfs(vector<vector<char>>& board, string& word, int r, int c, int idx, vector<vector<bool>>& visited) {
if (idx == word.size()) return true;
int rows = board.size(), cols = board[0].size();
if (r < 0 || r >= rows || c < 0 || c >= cols || visited[r][c] || board[r][c] != word[idx])
return false;
visited[r][c] = true;
bool res = dfs(board, word, r+1, c, idx+1, visited) ||
dfs(board, word, r-1, c, idx+1, visited) ||
dfs(board, word, r, c+1, idx+1, visited) ||
dfs(board, word, r, c-1, idx+1, visited);
visited[r][c] = false;
return res;
}
bool exist(vector<vector<char>>& board, string& word) {
int rows = board.size(), cols = board[0].size();
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
vector<vector<bool>> visited(rows, vector<bool>(cols, false));
if (dfs(board, word, r, c, 0, visited))
return true;
}
}
return false;
}
vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
vector<string> result;
for (auto& word : words) {
if (exist(board, word)) {
result.push_back(word);
}
}
return result;
}
int main() {
vector<vector<char>> board = {
{'o','a','a','n'},
{'e','t','a','e'},
{'i','h','k','r'},
{'i','f','l','v'}
};
vector<string> words = {"oath", "pea", "eat", "rain"};
vector<string> res = findWords(board, words);
for (auto& w : res) cout << w << " ";
cout << endl;
return 0;
}bool dfs(vector<vector<char>>& board, string& word, int r, int c, int idx, vector<vector<bool>>& visited)DFS helper to check if word exists starting at (r,c)if (idx == word.size()) return true;All characters matched base casevisited[r][c] = true;Mark cell visited before recursionvisited[r][c] = false;Backtrack by unmarking after recursionfor (int r = 0; r < rows; r++)Try all cells as starting pointsfor (int c = 0; c < cols; c++)Try all cells as starting pointsvector<vector<bool>> visited(rows, vector<bool>(cols, false));Create fresh visited array for each DFS callfor (auto& word : words)Check each word independentlyif (dfs(board, word, r, c, 0, visited))If DFS finds word, return truefunction exist(board, word) {
const rows = board.length, cols = board[0].length;
function dfs(r, c, idx, visited) {
if (idx === word.length) return true;
if (r < 0 || r >= rows || c < 0 || c >= cols || visited[r][c] || board[r][c] !== word[idx])
return false;
visited[r][c] = true;
const res = dfs(r+1, c, idx+1, visited) || dfs(r-1, c, idx+1, visited) || dfs(r, c+1, idx+1, visited) || dfs(r, c-1, idx+1, visited);
visited[r][c] = false;
return res;
}
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const visited = Array.from({length: rows}, () => Array(cols).fill(false));
if (dfs(r, c, 0, visited)) return true;
}
}
return false;
}
function findWords(board, words) {
const result = [];
for (const word of words) {
if (exist(board, word)) {
result.push(word);
}
}
return result;
}
// Driver code
const board = [
['o','a','a','n'],
['e','t','a','e'],
['i','h','k','r'],
['i','f','l','v']
];
const words = ['oath','pea','eat','rain'];
console.log(findWords(board, words));function exist(board, word)Helper function to check if a single word existsfor (let r = 0; r < rows; r++)Try all rows as starting pointsfor (let c = 0; c < cols; c++)Try all columns as starting pointsconst visited = Array.from({length: rows}, () => Array(cols).fill(false));Create fresh visited array for each DFS callif (dfs(r, c, 0, visited)) return true;If DFS finds the word starting at this cell, return truefunction dfs(r, c, idx, visited)DFS helper to check if word exists starting at (r,c)if (idx === word.length) return true;Base case: all characters matchedvisited[r][c] = true;Mark cell visited before recursive callsvisited[r][c] = false;Backtrack by unmarking after recursionfor (const word of words)Check each word independentlyO(N * 4^L * W) where N = number of cells, L = max word length, W = number of wordsO(L) for recursion stack and visited arrayFor each word, we try starting from each cell and explore up to 4 directions recursively for length L, repeated for W words.
This approach is too slow for large inputs but is essential to understand before optimizing.
Intuition
Build a prefix tree (Trie) of all words, then backtrack on the board, pruning paths that don't lead to any word in the Trie.
Algorithm
- Build a Trie from the list of words.
- For each cell in the board, start backtracking:
- At each step, check if current prefix exists in Trie.
- If prefix not in Trie, backtrack immediately (prune).
- If a word ends at current Trie node, add it to result and mark as found.
- Mark visited cells to avoid reuse and backtrack properly.
from typing import List
class TrieNode:
def __init__(self):
self.children = {}
self.word = None
class Solution:
def buildTrie(self, words):
root = TrieNode()
for w in words:
node = root
for c in w:
if c not in node.children:
node.children[c] = TrieNode()
node = node.children[c]
node.word = w
return root
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
root = self.buildTrie(words)
rows, cols = len(board), len(board[0])
result = []
def backtrack(r, c, parent):
letter = board[r][c]
currNode = parent.children.get(letter)
if not currNode:
return
if currNode.word:
result.append(currNode.word)
currNode.word = None # avoid duplicates
board[r][c] = '#'
for dr, dc in [(1,0),(-1,0),(0,1),(0,-1)]:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] != '#':
backtrack(nr, nc, currNode)
board[r][c] = letter
# Optimization: prune leaf nodes
if not currNode.children:
parent.children.pop(letter)
for r in range(rows):
for c in range(cols):
backtrack(r, c, root)
return result
# Driver code
if __name__ == '__main__':
board = [
['o','a','a','n'],
['e','t','a','e'],
['i','h','k','r'],
['i','f','l','v']
]
words = ['oath','pea','eat','rain']
sol = Solution()
print(sol.findWords(board, words))class TrieNode:Defines Trie node structure with children and optional worddef buildTrie(self, words):Builds Trie from all words for prefix lookupnode.word = wMarks end of a word in Trie nodedef backtrack(r, c, parent):Backtracking function exploring board with Trie pruningcurrNode = parent.children.get(letter)Check if current letter continues a valid prefixif currNode.word:Found a complete word, add to result and avoid duplicatesboard[r][c] = '#'Mark cell visited to avoid reuseboard[r][c] = letterRestore cell after backtrackingif not currNode.children: parent.children.pop(letter)Prune leaf nodes to optimize Trie size during searchfor r in range(rows):Start backtracking from every cellimport java.util.*;
class TrieNode {
Map<Character, TrieNode> children = new HashMap<>();
String word = null;
}
public class WordSearchTrie {
private TrieNode buildTrie(String[] words) {
TrieNode root = new TrieNode();
for (String w : words) {
TrieNode node = root;
for (char c : w.toCharArray()) {
node.children.putIfAbsent(c, new TrieNode());
node = node.children.get(c);
}
node.word = w;
}
return root;
}
private void backtrack(char[][] board, int r, int c, TrieNode parent, List<String> result) {
char letter = board[r][c];
TrieNode currNode = parent.children.get(letter);
if (currNode == null) return;
if (currNode.word != null) {
result.add(currNode.word);
currNode.word = null; // avoid duplicates
}
board[r][c] = '#';
int[] rowOffsets = {1, -1, 0, 0};
int[] colOffsets = {0, 0, 1, -1};
for (int i = 0; i < 4; i++) {
int nr = r + rowOffsets[i], nc = c + colOffsets[i];
if (nr >= 0 && nr < board.length && nc >= 0 && nc < board[0].length && board[nr][nc] != '#') {
backtrack(board, nr, nc, currNode, result);
}
}
board[r][c] = letter;
if (currNode.children.isEmpty()) {
parent.children.remove(letter);
}
}
public List<String> findWords(char[][] board, String[] words) {
List<String> result = new ArrayList<>();
TrieNode root = buildTrie(words);
for (int r = 0; r < board.length; r++) {
for (int c = 0; c < board[0].length; c++) {
backtrack(board, r, c, root, result);
}
}
return result;
}
public static void main(String[] args) {
WordSearchTrie sol = new WordSearchTrie();
char[][] board = {
{'o','a','a','n'},
{'e','t','a','e'},
{'i','h','k','r'},
{'i','f','l','v'}
};
String[] words = {"oath", "pea", "eat", "rain"};
System.out.println(sol.findWords(board, words));
}
}class TrieNode {Defines Trie node with children map and optional wordTrieNode root = new TrieNode();Root of the Trienode.children.putIfAbsent(c, new TrieNode());Insert character nodes if missingif (currNode.word != null) {Found a complete word, add to result and avoid duplicatesboard[r][c] = '#';Mark cell visited to prevent reuseboard[r][c] = letter;Restore cell after backtrackingif (currNode.children.isEmpty()) { parent.children.remove(letter); }Prune leaf nodes to optimize Triefor (int r = 0; r < board.length; r++)Start backtracking from every cell#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
using namespace std;
struct TrieNode {
unordered_map<char, TrieNode*> children;
string word = "";
};
class Solution {
public:
TrieNode* buildTrie(vector<string>& words) {
TrieNode* root = new TrieNode();
for (auto& w : words) {
TrieNode* node = root;
for (char c : w) {
if (!node->children.count(c))
node->children[c] = new TrieNode();
node = node->children[c];
}
node->word = w;
}
return root;
}
void backtrack(vector<vector<char>>& board, int r, int c, TrieNode* parent, vector<string>& result) {
char letter = board[r][c];
if (!parent->children.count(letter)) return;
TrieNode* currNode = parent->children[letter];
if (!currNode->word.empty()) {
result.push_back(currNode->word);
currNode->word = ""; // avoid duplicates
}
board[r][c] = '#';
vector<pair<int,int>> directions = {{1,0},{-1,0},{0,1},{0,-1}};
for (auto& d : directions) {
int nr = r + d.first, nc = c + d.second;
if (nr >= 0 && nr < board.size() && nc >= 0 && nc < board[0].size() && board[nr][nc] != '#') {
backtrack(board, nr, nc, currNode, result);
}
}
board[r][c] = letter;
if (currNode->children.empty()) {
parent->children.erase(letter);
delete currNode;
}
}
vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
vector<string> result;
TrieNode* root = buildTrie(words);
for (int r = 0; r < board.size(); r++) {
for (int c = 0; c < board[0].size(); c++) {
backtrack(board, r, c, root, result);
}
}
return result;
}
};
int main() {
vector<vector<char>> board = {
{'o','a','a','n'},
{'e','t','a','e'},
{'i','h','k','r'},
{'i','f','l','v'}
};
vector<string> words = {"oath", "pea", "eat", "rain"};
Solution sol;
vector<string> res = sol.findWords(board, words);
for (auto& w : res) cout << w << " ";
cout << endl;
return 0;
}struct TrieNode {Trie node with children map and optional word stringTrieNode* root = new TrieNode();Root node of Trieif (!node->children.count(c))Insert new child node if missingif (!currNode->word.empty()) {Found a word, add to result and avoid duplicatesboard[r][c] = '#';Mark cell visited to avoid reuseboard[r][c] = letter;Restore cell after backtrackingif (currNode->children.empty()) {Prune leaf nodes to optimize Trie sizefor (int r = 0; r < board.size(); r++)Start backtracking from every cellclass TrieNode {
constructor() {
this.children = new Map();
this.word = null;
}
}
function buildTrie(words) {
const root = new TrieNode();
for (const w of words) {
let node = root;
for (const c of w) {
if (!node.children.has(c)) {
node.children.set(c, new TrieNode());
}
node = node.children.get(c);
}
node.word = w;
}
return root;
}
function findWords(board, words) {
const root = buildTrie(words);
const rows = board.length, cols = board[0].length;
const result = [];
function backtrack(r, c, parent) {
const letter = board[r][c];
const currNode = parent.children.get(letter);
if (!currNode) return;
if (currNode.word !== null) {
result.push(currNode.word);
currNode.word = null; // avoid duplicates
}
board[r][c] = '#';
const directions = [[1,0],[-1,0],[0,1],[0,-1]];
for (const [dr, dc] of directions) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && board[nr][nc] !== '#') {
backtrack(nr, nc, currNode);
}
}
board[r][c] = letter;
if (currNode.children.size === 0) {
parent.children.delete(letter);
}
}
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
backtrack(r, c, root);
}
}
return result;
}
// Driver code
const board = [
['o','a','a','n'],
['e','t','a','e'],
['i','h','k','r'],
['i','f','l','v']
];
const words = ['oath','pea','eat','rain'];
console.log(findWords(board, words));class TrieNode {Defines Trie node with children map and optional wordfunction buildTrie(words) {Builds Trie from words for prefix pruningnode.word = w;Marks end of a word in Trieif (currNode.word !== null) {Found a word, add to result and avoid duplicatesboard[r][c] = '#';Mark cell visited to avoid reuseboard[r][c] = letter;Restore cell after backtrackingif (currNode.children.size === 0) { parent.children.delete(letter); }Prune leaf nodes to optimize Triefor (let r = 0; r < rows; r++)Start backtracking from every cellO(M * 4 * 3^(L-1)) where M = number of cells, L = max word lengthO(N) for Trie nodes + O(L) recursion stackTrie pruning reduces search space drastically by cutting off invalid prefixes early, so not all paths are explored.
This is the standard efficient solution expected in interviews for this problem.
Intuition
Mark visited cells directly on the board to avoid extra space, and remove Trie nodes when no longer needed to speed up future searches.
Algorithm
- Build a Trie from the words.
- For each cell, backtrack using the Trie:
- Mark the board cell as visited by replacing with a special char.
- If a word is found, add to result and remove from Trie.
- After exploring neighbors, restore the cell.
- Remove Trie nodes with no children to prune search space.
from typing import List
class TrieNode:
def __init__(self):
self.children = {}
self.word = None
class Solution:
def buildTrie(self, words):
root = TrieNode()
for w in words:
node = root
for c in w:
if c not in node.children:
node.children[c] = TrieNode()
node = node.children[c]
node.word = w
return root
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
root = self.buildTrie(words)
rows, cols = len(board), len(board[0])
result = []
def backtrack(r, c, node):
letter = board[r][c]
currNode = node.children.get(letter)
if not currNode:
return
if currNode.word:
result.append(currNode.word)
currNode.word = None
board[r][c] = '#'
for dr, dc in [(1,0),(-1,0),(0,1),(0,-1)]:
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols and board[nr][nc] != '#':
backtrack(nr, nc, currNode)
board[r][c] = letter
if not currNode.children:
node.children.pop(letter)
for r in range(rows):
for c in range(cols):
backtrack(r, c, root)
return result
# Driver code
if __name__ == '__main__':
board = [
['o','a','a','n'],
['e','t','a','e'],
['i','h','k','r'],
['i','f','l','v']
]
words = ['oath','pea','eat','rain']
sol = Solution()
print(sol.findWords(board, words))board[r][c] = '#'In-place marking to save space instead of separate visited arrayboard[r][c] = letterRestore original letter after backtrackingif not currNode.children: node.children.pop(letter)Prune Trie nodes aggressively to reduce search spacecurrNode.word = NoneAvoid duplicate word additionsfor dr, dc in [(1,0),(-1,0),(0,1),(0,-1)]:Explore all four directionsfor r in range(rows):Start backtracking from every cellimport java.util.*;
class TrieNode {
Map<Character, TrieNode> children = new HashMap<>();
String word = null;
}
public class WordSearchTrieOptimized {
private TrieNode buildTrie(String[] words) {
TrieNode root = new TrieNode();
for (String w : words) {
TrieNode node = root;
for (char c : w.toCharArray()) {
node.children.putIfAbsent(c, new TrieNode());
node = node.children.get(c);
}
node.word = w;
}
return root;
}
private void backtrack(char[][] board, int r, int c, TrieNode node, List<String> result) {
char letter = board[r][c];
TrieNode currNode = node.children.get(letter);
if (currNode == null) return;
if (currNode.word != null) {
result.add(currNode.word);
currNode.word = null;
}
board[r][c] = '#';
int[] rowOffsets = {1, -1, 0, 0};
int[] colOffsets = {0, 0, 1, -1};
for (int i = 0; i < 4; i++) {
int nr = r + rowOffsets[i], nc = c + colOffsets[i];
if (nr >= 0 && nr < board.length && nc >= 0 && nc < board[0].length && board[nr][nc] != '#') {
backtrack(board, nr, nc, currNode, result);
}
}
board[r][c] = letter;
if (currNode.children.isEmpty()) {
node.children.remove(letter);
}
}
public List<String> findWords(char[][] board, String[] words) {
List<String> result = new ArrayList<>();
TrieNode root = buildTrie(words);
for (int r = 0; r < board.length; r++) {
for (int c = 0; c < board[0].length; c++) {
backtrack(board, r, c, root, result);
}
}
return result;
}
public static void main(String[] args) {
WordSearchTrieOptimized sol = new WordSearchTrieOptimized();
char[][] board = {
{'o','a','a','n'},
{'e','t','a','e'},
{'i','h','k','r'},
{'i','f','l','v'}
};
String[] words = {"oath", "pea", "eat", "rain"};
System.out.println(sol.findWords(board, words));
}
}board[r][c] = '#'In-place marking to save spaceboard[r][c] = letter;Restore after backtrackingif (currNode.children.isEmpty()) { node.children.remove(letter); }Prune Trie nodes aggressivelycurrNode.word = null;Avoid duplicate word additionsfor (int i = 0; i < 4; i++)Explore all four directionsfor (int r = 0; r < board.length; r++)Start backtracking from every cell#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
using namespace std;
struct TrieNode {
unordered_map<char, TrieNode*> children;
string word = "";
};
class Solution {
public:
TrieNode* buildTrie(vector<string>& words) {
TrieNode* root = new TrieNode();
for (auto& w : words) {
TrieNode* node = root;
for (char c : w) {
if (!node->children.count(c))
node->children[c] = new TrieNode();
node = node->children[c];
}
node->word = w;
}
return root;
}
void backtrack(vector<vector<char>>& board, int r, int c, TrieNode* node, vector<string>& result) {
char letter = board[r][c];
if (!node->children.count(letter)) return;
TrieNode* currNode = node->children[letter];
if (!currNode->word.empty()) {
result.push_back(currNode->word);
currNode->word = "";
}
board[r][c] = '#';
vector<pair<int,int>> directions = {{1,0},{-1,0},{0,1},{0,-1}};
for (auto& d : directions) {
int nr = r + d.first, nc = c + d.second;
if (nr >= 0 && nr < board.size() && nc >= 0 && nc < board[0].size() && board[nr][nc] != '#') {
backtrack(board, nr, nc, currNode, result);
}
}
board[r][c] = letter;
if (currNode->children.empty()) {
node->children.erase(letter);
delete currNode;
}
}
vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
vector<string> result;
TrieNode* root = buildTrie(words);
for (int r = 0; r < board.size(); r++) {
for (int c = 0; c < board[0].size(); c++) {
backtrack(board, r, c, root, result);
}
}
return result;
}
};
int main() {
vector<vector<char>> board = {
{'o','a','a','n'},
{'e','t','a','e'},
{'i','h','k','r'},
{'i','f','l','v'}
};
vector<string> words = {"oath", "pea", "eat", "rain"};
Solution sol;
vector<string> res = sol.findWords(board, words);
for (auto& w : res) cout << w << " ";
cout << endl;
return 0;
}board[r][c] = '#';In-place marking to save spaceboard[r][c] = letter;Restore after backtrackingif (currNode->children.empty()) {Prune Trie nodes aggressivelycurrNode->word = "";Avoid duplicate word additionsfor (auto& d : directions)Explore all four directionsfor (int r = 0; r < board.size(); r++)Start backtracking from every cellclass TrieNode {
constructor() {
this.children = new Map();
this.word = null;
}
}
function buildTrie(words) {
const root = new TrieNode();
for (const w of words) {
let node = root;
for (const c of w) {
if (!node.children.has(c)) {
node.children.set(c, new TrieNode());
}
node = node.children.get(c);
}
node.word = w;
}
return root;
}
function findWords(board, words) {
const root = buildTrie(words);
const rows = board.length, cols = board[0].length;
const result = [];
function backtrack(r, c, node) {
const letter = board[r][c];
const currNode = node.children.get(letter);
if (!currNode) return;
if (currNode.word !== null) {
result.push(currNode.word);
currNode.word = null;
}
board[r][c] = '#';
const directions = [[1,0],[-1,0],[0,1],[0,-1]];
for (const [dr, dc] of directions) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && board[nr][nc] !== '#') {
backtrack(nr, nc, currNode);
}
}
board[r][c] = letter;
if (currNode.children.size === 0) {
node.children.delete(letter);
}
}
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
backtrack(r, c, root);
}
}
return result;
}
// Driver code
const board = [
['o','a','a','n'],
['e','t','a','e'],
['i','h','k','r'],
['i','f','l','v']
];
const words = ['oath','pea','eat','rain'];
console.log(findWords(board, words));board[r][c] = '#';In-place marking to save spaceboard[r][c] = letter;Restore after backtrackingif (currNode.children.size === 0) { node.children.delete(letter); }Prune Trie nodes aggressivelycurrNode.word = null;Avoid duplicate word additionsfor (const [dr, dc] of directions)Explore all four directionsfor (let r = 0; r < rows; r++)Start backtracking from every cellO(M * 4 * 3^(L-1)) similar to previous approach but with less overheadO(N) for Trie + O(L) recursion stack, less auxiliary space due to in-place markingIn-place marking and pruning reduce memory and runtime overhead, making it the most efficient practical approach.
This is the recommended approach to implement in interviews for best performance.
| Approach | Time | Space | Stack Risk | Reconstruct | Use In Interview |
|---|---|---|---|---|---|
| 1. Brute Force | O(W * N * 4^L) | O(L) recursion stack + O(N) visited | Yes (deep recursion for long words) | N/A | Mention only - too slow to implement |
| 2. Backtracking with Trie | O(M * 4 * 3^(L-1)) | O(N) Trie + O(L) recursion stack | Yes | Yes | Optimal approach to implement |
| 3. Backtracking with Trie + In-place Marking + Pruning | O(M * 4 * 3^(L-1)) with less overhead | O(N) Trie + O(L) recursion stack, less auxiliary space | Yes | Yes | Best practical solution to implement |
How to Present
Step 1: Clarify problem constraints and examples.Step 2: Describe brute force approach to show understanding.Step 3: Introduce Trie to optimize prefix checks and prune search.Step 4: Explain in-place marking and Trie pruning for best performance.Step 5: Code the optimal solution and test edge cases.
Time Allocation
What the Interviewer Tests
Ability to identify brute force limitations, knowledge of Trie data structure, backtracking skills, and optimization techniques.
Common Follow-ups
- How to handle very large word lists? → Use Trie and pruning to scale.
- Can diagonal moves be allowed? → Modify directions array accordingly.
- How to avoid duplicates in result? → Mark found words as null in Trie.
- What if board is very large? → Early pruning and iterative DFS can help.
When to Use
1. Need to find multiple words in a grid 2. Words can be formed by adjacent letters 3. Large word list requiring efficient prefix checks 4. Backtracking with pruning is needed
