Bird
Raised Fist0
Interview Prepchallenge-problemshardAmazonFacebookGoogleBloomberg

Word Search II

Choose your preparation mode4 modes available
🎯
Word Search II
hardMIXEDAmazonFacebookGoogle

Imagine you have a large grid of letters and a dictionary of words. Your task is to find all words from the dictionary that can be formed by tracing adjacent letters in the grid, like a word puzzle challenge.

💡 This problem combines searching in a 2D grid with efficient prefix checking. Beginners often struggle because naive search is too slow, and understanding how to prune searches using tries is key to solving it efficiently.
📋
Problem Statement

Given a 2D board of characters and a list of words, find all words in 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. Return all words found in the board.

1 <= board.length <= 121 <= board[i].length <= 121 <= words.length <= 3 * 10^41 <= words[i].length <= 10board and words[i] consist of lowercase English letters
💡
Example
Input"board = [[\"o\",\"a\",\"a\",\"n\"],[\"e\",\"t\",\"a\",\"e\"],[\"i\",\"h\",\"k\",\"r\"],[\"i\",\"f\",\"l\",\"v\"]], words = [\"oath\",\"pea\",\"eat\",\"rain\"]"
Output["oath","eat"]

The words 'eat' and 'oath' can be formed by adjacent letters in the board, while 'pea' and 'rain' cannot.

  • Empty board → returns empty list
  • Empty words list → returns empty list
  • Words with repeated letters that require revisiting cells → should not be found
  • Board with all identical letters and words that are substrings → finds correct matches
⚠️
Common Mistakes
Not marking visited cells properly

Leads to revisiting same cell multiple times causing incorrect matches or infinite recursion

✅ Use a visited set or in-place marking to track visited cells during backtracking

Not pruning trie nodes after word found

Leads to redundant searches and slower runtime

✅ Remove trie nodes with no children after word is found to reduce search space

Returning duplicates of found words

Output contains repeated words

✅ Mark found words as None/null in trie to avoid adding duplicates

Not handling empty board or words list

Code may crash or return incorrect results

✅ Add checks for empty inputs and return empty list early

Modifying board without restoring

Leads to incorrect results for subsequent searches

✅ Always restore board cell after backtracking recursion

🧠
Brute Force (Backtracking for Each Word)
💡 This approach tries to find each word independently by searching the entire board for its first letter and then backtracking to check if the word can be formed. It is simple but inefficient for large inputs.

Intuition

For each word, scan the board to find starting points matching the first letter, then recursively explore neighbors to match subsequent letters, marking visited cells to avoid reuse.

Algorithm

  1. For each word in the list:
  2. For each cell in the board:
  3. If the cell matches the first letter of the word, start backtracking from there.
  4. Backtracking explores adjacent cells recursively to match the next letter, marking cells visited to avoid reuse.
💡 The nested loops and recursion make it hard to see the exponential time complexity at first, but it's a straightforward exhaustive search.
</>
Code
from typing import List

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, word, index, visited):
            if index == len(word):
                return True
            if r < 0 or c < 0 or r >= ROWS or c >= COLS:
                return False
            if (r, c) in visited or board[r][c] != word[index]:
                return False

            visited.add((r, c))
            # Explore neighbors
            found = (backtrack(r+1, c, word, index+1, visited) or
                     backtrack(r-1, c, word, index+1, visited) or
                     backtrack(r, c+1, word, index+1, visited) or
                     backtrack(r, c-1, word, index+1, visited))
            visited.remove((r, c))
            return found

        for word in words:
            found_word = False
            for r in range(ROWS):
                for c in range(COLS):
                    if board[r][c] == word[0]:
                        if backtrack(r, c, word, 0, set()):
                            res.append(word)
                            found_word = True
                            break
                if found_word:
                    break
        return res

# Example usage:
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))
Line Notes
def backtrack(r, c, word, index, visited):Defines recursive function to explore board for current word index to match letters sequentially.
if index == len(word):Base case: all letters matched successfully, so return True to indicate word found.
if (r, c) in visited or board[r][c] != word[index]:Prune paths that revisit cells or where current cell letter doesn't match the word's current letter.
visited.add((r, c))Mark current cell as visited to avoid reuse in the same word path.
visited.remove((r, c))Backtrack by unmarking cell after exploring all neighbors to allow other paths to use it.
for word in words:Iterate over all words independently, performing a fresh search for each.
if backtrack(r, c, word, 0, set()):Start backtracking from cells matching the first letter of the current word.
import java.util.*;

public class Solution {
    private int ROWS, COLS;
    private char[][] board;

    public List<String> findWords(char[][] board, String[] words) {
        this.board = board;
        ROWS = board.length;
        COLS = board[0].length;
        List<String> res = new ArrayList<>();

        for (String word : words) {
            boolean found = false;
            for (int r = 0; r < ROWS && !found; r++) {
                for (int c = 0; c < COLS && !found; c++) {
                    if (board[r][c] == word.charAt(0)) {
                        if (backtrack(r, c, word, 0, new boolean[ROWS][COLS])) {
                            res.add(word);
                            found = true;
                        }
                    }
                }
            }
        }
        return res;
    }

    private boolean backtrack(int r, int c, String word, int index, boolean[][] visited) {
        if (index == word.length()) return true;
        if (r < 0 || c < 0 || r >= ROWS || c >= COLS) return false;
        if (visited[r][c] || board[r][c] != word.charAt(index)) return false;

        visited[r][c] = true;
        boolean found = backtrack(r + 1, c, word, index + 1, visited) ||
                        backtrack(r - 1, c, word, index + 1, visited) ||
                        backtrack(r, c + 1, word, index + 1, visited) ||
                        backtrack(r, c - 1, word, index + 1, visited);
        visited[r][c] = false;
        return found;
    }

    // Example usage
    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"};
        Solution sol = new Solution();
        System.out.println(sol.findWords(board, words));
    }
}
Line Notes
private boolean backtrack(int r, int c, String word, int index, boolean[][] visited)Recursive helper to explore board for current word index, checking boundaries and visited cells.
if (index == word.length()) return true;Base case: all letters matched successfully.
if (visited[r][c] || board[r][c] != word.charAt(index)) return false;Prune invalid paths where cell is visited or letter mismatch.
visited[r][c] = true;Mark cell visited before recursion to avoid reuse.
visited[r][c] = false;Unmark cell after recursion to backtrack and allow other paths.
for (String word : words)Iterate over all words independently to find each separately.
if (board[r][c] == word.charAt(0))Start backtracking from cells matching the first letter of the word.
#include <iostream>
#include <vector>
#include <string>
#include <unordered_set>
using namespace std;

class Solution {
    int ROWS, COLS;
    vector<vector<char>> board;

    bool backtrack(int r, int c, const string& word, int index, vector<vector<bool>>& visited) {
        if (index == word.size()) return true;
        if (r < 0 || c < 0 || r >= ROWS || c >= COLS) return false;
        if (visited[r][c] || board[r][c] != word[index]) return false;

        visited[r][c] = true;
        bool found = backtrack(r+1, c, word, index+1, visited) ||
                     backtrack(r-1, c, word, index+1, visited) ||
                     backtrack(r, c+1, word, index+1, visited) ||
                     backtrack(r, c-1, word, index+1, visited);
        visited[r][c] = false;
        return found;
    }

public:
    vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
        this->board = board;
        ROWS = board.size();
        COLS = board[0].size();
        vector<string> res;

        for (const string& word : words) {
            bool found = false;
            for (int r = 0; r < ROWS && !found; r++) {
                for (int c = 0; c < COLS && !found; c++) {
                    if (board[r][c] == word[0]) {
                        vector<vector<bool>> visited(ROWS, vector<bool>(COLS, false));
                        if (backtrack(r, c, word, 0, visited)) {
                            res.push_back(word);
                            found = true;
                        }
                    }
                }
            }
        }
        return res;
    }
};

// Example usage
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> result = sol.findWords(board, words);
    for (const string& w : result) {
        cout << w << " ";
    }
    cout << endl;
    return 0;
}
Line Notes
bool backtrack(int r, int c, const string& word, int index, vector<vector<bool>>& visited)Recursive function exploring board for current word index, checking boundaries and visited cells.
if (index == word.size()) return true;Base case: all letters matched successfully.
if (visited[r][c] || board[r][c] != word[index]) return false;Prune invalid or revisited cells to avoid wrong paths.
visited[r][c] = true;Mark cell visited before recursion to avoid reuse.
visited[r][c] = false;Unmark cell after recursion to backtrack and allow other paths.
for (const string& word : words)Iterate over all words independently to find each separately.
if (board[r][c] == word[0])Start backtracking from cells matching the first letter of the word.
var findWords = function(board, words) {
    const ROWS = board.length, COLS = board[0].length;
    const res = [];

    function backtrack(r, c, word, index, visited) {
        if (index === word.length) return true;
        if (r < 0 || c < 0 || r >= ROWS || c >= COLS) return false;
        if (visited.has(r + ',' + c) || board[r][c] !== word[index]) return false;

        visited.add(r + ',' + c);
        const found = backtrack(r+1, c, word, index+1, visited) ||
                      backtrack(r-1, c, word, index+1, visited) ||
                      backtrack(r, c+1, word, index+1, visited) ||
                      backtrack(r, c-1, word, index+1, visited);
        visited.delete(r + ',' + c);
        return found;
    }

    for (const word of words) {
        let foundWord = false;
        for (let r = 0; r < ROWS && !foundWord; r++) {
            for (let c = 0; c < COLS && !foundWord; c++) {
                if (board[r][c] === word[0]) {
                    if (backtrack(r, c, word, 0, new Set())) {
                        res.push(word);
                        foundWord = true;
                    }
                }
            }
        }
    }
    return res;
};

// Example usage
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));
Line Notes
function backtrack(r, c, word, index, visited)Recursive helper to explore board for current word index, checking boundaries and visited cells.
if (index === word.length) return true;Base case: all letters matched successfully.
if (visited.has(r + ',' + c) || board[r][c] !== word[index]) return false;Prune invalid or revisited cells to avoid wrong paths.
visited.add(r + ',' + c);Mark cell visited before recursion to avoid reuse.
visited.delete(r + ',' + c);Unmark cell after recursion to backtrack and allow other paths.
for (const word of words)Iterate over all words independently to find each separately.
if (board[r][c] === word[0])Start backtracking from cells matching the first letter of the word.
⏱
Complexity
TimeO(W * N * M * 4^L) where N and M are board dimensions, L is max word length, W is number of words
SpaceO(L) recursion stack + O(L) visited set per search

For each word, we scan the board and do a DFS that can explore up to 4 directions per letter, leading to exponential time in word length. Doing this for all words multiplies by W.

💡 For a 12x12 board and 10-letter words, this approach tries billions of paths per word, making it impractical for large word lists.
Interview Verdict: TLE

This approach is too slow for large inputs but is useful to understand the problem and correctness before optimizing.

🧠
Backtracking with Trie for Prefix Pruning
💡 This approach builds a trie from the word list to quickly check prefixes during backtracking, pruning impossible paths early and avoiding redundant searches.

Intuition

Instead of searching for each word separately, build a prefix tree (trie) of all words. While exploring the board, check if the current path matches any prefix in the trie. If not, backtrack immediately.

Algorithm

  1. Build a trie from the list of words.
  2. For each cell in the board, start backtracking if the letter is a child in the trie root.
  3. During backtracking, move down the trie nodes matching letters.
  4. If a trie node marks a complete word, add it to results and mark it found to avoid duplicates.
  5. Prune paths that do not exist in the trie.
💡 The key insight is using the trie to avoid exploring paths that cannot form any word, drastically reducing search space.
</>
Code
from typing import List

class TrieNode:
    def __init__(self):
        self.children = {}
        self.word = None

class Solution:
    def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
        root = TrieNode()
        for word in words:
            node = root
            for char in word:
                if char not in node.children:
                    node.children[char] = TrieNode()
                node = node.children[char]
            node.word = word

        ROWS, COLS = len(board), len(board[0])
        res = []

        def backtrack(r, c, parent):
            letter = board[r][c]
            curr_node = parent.children.get(letter)
            if not curr_node:
                return

            if curr_node.word:
                res.append(curr_node.word)
                curr_node.word = None  # avoid duplicates

            board[r][c] = '#'
            for (nr, nc) in [(r+1,c),(r-1,c),(r,c+1),(r,c-1)]:
                if 0 <= nr < ROWS and 0 <= nc < COLS and board[nr][nc] != '#':
                    backtrack(nr, nc, curr_node)
            board[r][c] = letter

            # Optimization: prune leaf nodes
            if not curr_node.children:
                parent.children.pop(letter)

        for r in range(ROWS):
            for c in range(COLS):
                backtrack(r, c, root)

        return res

# Example usage
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))
Line Notes
class TrieNode:Defines trie node structure with children dictionary and word marker to store complete words.
for word in words:Build trie by inserting all words character by character.
if curr_node.word:Found a complete word at current trie node; add to results and mark None to avoid duplicates.
board[r][c] = '#'Mark cell visited in-place to save extra space for visited tracking.
board[r][c] = letterRestore cell letter after backtracking to maintain board state.
if not curr_node.children:Prune leaf nodes from trie to optimize search by removing paths that no longer lead to words.
for r in range(ROWS):Start backtracking from every cell in the board to find all possible words.
import java.util.*;

class TrieNode {
    Map<Character, TrieNode> children = new HashMap<>();
    String word = null;
}

public class Solution {
    private int ROWS, COLS;
    private char[][] board;
    private List<String> res = new ArrayList<>();

    public List<String> findWords(char[][] board, String[] words) {
        this.board = board;
        ROWS = board.length;
        COLS = board[0].length;

        TrieNode root = new TrieNode();
        for (String word : words) {
            TrieNode node = root;
            for (char c : word.toCharArray()) {
                node.children.putIfAbsent(c, new TrieNode());
                node = node.children.get(c);
            }
            node.word = word;
        }

        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                backtrack(r, c, root);
            }
        }
        return res;
    }

    private void backtrack(int r, int c, TrieNode parent) {
        char letter = board[r][c];
        TrieNode currNode = parent.children.get(letter);
        if (currNode == null) return;

        if (currNode.word != null) {
            res.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 < ROWS && nc >= 0 && nc < COLS && board[nr][nc] != '#') {
                backtrack(nr, nc, currNode);
            }
        }
        board[r][c] = letter;

        if (currNode.children.isEmpty()) {
            parent.children.remove(letter);
        }
    }

    // Example usage
    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"};
        Solution sol = new Solution();
        System.out.println(sol.findWords(board, words));
    }
}
Line Notes
class TrieNode {Trie node with children map and word marker to store complete words.
for (String word : words) {Build trie from all words by inserting characters.
if (currNode.word != null) {Found a word at current node; add to results and mark null to avoid duplicates.
board[r][c] = '#';Mark cell visited in-place to avoid extra memory usage.
board[r][c] = letter;Restore cell letter after recursion to maintain board state.
if (currNode.children.isEmpty()) {Prune leaf nodes from trie to optimize search space.
for (int r = 0; r < ROWS; r++) {Start backtracking from every cell in the board.
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
using namespace std;

struct TrieNode {
    unordered_map<char, TrieNode*> children;
    string word = "";
};

class Solution {
    int ROWS, COLS;
    vector<vector<char>> board;
    vector<string> res;

    void insertWord(TrieNode* root, const string& word) {
        TrieNode* node = root;
        for (char c : word) {
            if (!node->children.count(c)) {
                node->children[c] = new TrieNode();
            }
            node = node->children[c];
        }
        node->word = word;
    }

    void backtrack(int r, int c, TrieNode* parent) {
        char letter = board[r][c];
        if (!parent->children.count(letter)) return;
        TrieNode* currNode = parent->children[letter];

        if (!currNode->word.empty()) {
            res.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 < ROWS && nc >= 0 && nc < COLS && board[nr][nc] != '#') {
                backtrack(nr, nc, currNode);
            }
        }
        board[r][c] = letter;

        if (currNode->children.empty()) {
            parent->children.erase(letter);
            delete currNode;
        }
    }

public:
    vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
        this->board = board;
        ROWS = board.size();
        COLS = board[0].size();
        TrieNode* root = new TrieNode();
        for (const string& word : words) {
            insertWord(root, word);
        }

        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                backtrack(r, c, root);
            }
        }
        return res;
    }
};

// Example usage
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> result = sol.findWords(board, words);
    for (const string& w : result) {
        cout << w << " ";
    }
    cout << endl;
    return 0;
}
Line Notes
struct TrieNode {Trie node with children map and word string marker to store complete words.
void insertWord(TrieNode* root, const string& word)Insert words into trie character by character.
if (!currNode->word.empty()) {Found a word at current node; add to results and clear to avoid duplicates.
board[r][c] = '#';Mark cell visited in-place to save extra space.
board[r][c] = letter;Restore cell letter after recursion to maintain board state.
if (currNode->children.empty()) {Prune leaf nodes and free memory to optimize search.
for (int r = 0; r < ROWS; r++) {Start backtracking from every cell in the board.
class TrieNode {
    constructor() {
        this.children = new Map();
        this.word = null;
    }
}

var findWords = function(board, words) {
    const root = new TrieNode();
    for (const word of words) {
        let node = root;
        for (const char of word) {
            if (!node.children.has(char)) {
                node.children.set(char, new TrieNode());
            }
            node = node.children.get(char);
        }
        node.word = word;
    }

    const ROWS = board.length, COLS = board[0].length;
    const res = [];

    function backtrack(r, c, parent) {
        const letter = board[r][c];
        const currNode = parent.children.get(letter);
        if (!currNode) return;

        if (currNode.word !== null) {
            res.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 res;
};

// Example usage
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));
Line Notes
class TrieNode {Trie node with children map and word marker to store complete words.
for (const word of words) {Build trie from all words by inserting characters.
if (currNode.word !== null) {Found a word at current node; add to results and mark null to avoid duplicates.
board[r][c] = '#';Mark cell visited in-place to save extra space.
board[r][c] = letter;Restore cell letter after recursion to maintain board state.
if (currNode.children.size === 0) {Prune leaf nodes from trie to optimize search space.
for (let r = 0; r < ROWS; r++) {Start backtracking from every cell in the board.
⏱
Complexity
TimeO(N*M*4^L) where N and M are board dimensions and L is max word length
SpaceO(W*L) for trie + O(L) recursion stack

Trie reduces redundant searches by pruning invalid prefixes early, so backtracking explores only valid paths. The pruning and trie construction dominate complexity.

💡 For a 12x12 board and 10-letter words, this approach is much faster than brute force because it avoids exploring impossible paths.
Interview Verdict: Accepted

This is the standard efficient solution for Word Search II, balancing complexity and implementation.

🧠
Backtracking with Trie and In-Place Board Modification (Space Optimized)
💡 This approach is a space-optimized variant of the trie backtracking method, using the board itself to mark visited cells instead of extra memory, and pruning the trie aggressively to reduce search space.

Intuition

Mark visited cells by temporarily changing their value in the board, and prune trie nodes when no longer needed to save memory and speed up search.

Algorithm

  1. Build a trie from the word list.
  2. For each cell in the board, start backtracking if the letter is a child in the trie root.
  3. Mark the current cell as visited by replacing its letter with a special character.
  4. If a word is found at the current trie node, add it to results and remove it from the trie.
  5. After exploring neighbors, restore the original letter and prune trie nodes with no children.
💡 Using the board itself to mark visited cells avoids extra space, and pruning the trie reduces unnecessary future searches.
</>
Code
from typing import List

class TrieNode:
    def __init__(self):
        self.children = {}
        self.word = None

class Solution:
    def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
        root = TrieNode()
        for word in words:
            node = root
            for char in word:
                if char not in node.children:
                    node.children[char] = TrieNode()
                node = node.children[char]
            node.word = word

        ROWS, COLS = len(board), len(board[0])
        res = []

        def backtrack(r, c, parent):
            letter = board[r][c]
            curr_node = parent.children.get(letter)
            if not curr_node:
                return

            if curr_node.word:
                res.append(curr_node.word)
                curr_node.word = None

            board[r][c] = '#'
            for (nr, nc) in [(r+1,c),(r-1,c),(r,c+1),(r,c-1)]:
                if 0 <= nr < ROWS and 0 <= nc < COLS and board[nr][nc] != '#':
                    backtrack(nr, nc, curr_node)
            board[r][c] = letter

            if not curr_node.children:
                parent.children.pop(letter)

        for r in range(ROWS):
            for c in range(COLS):
                backtrack(r, c, root)

        return res

# Example usage
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))
Line Notes
board[r][c] = '#'Mark cell visited in-place to save space instead of using extra visited set.
board[r][c] = letterRestore cell letter after recursion to maintain board state for other paths.
if curr_node.word:Add found word to results and mark None to avoid duplicates.
if not curr_node.children:Prune trie nodes with no children to optimize search and reduce trie size.
for r in range(ROWS):Start backtracking from every cell in the board to find all words.
import java.util.*;

class TrieNode {
    Map<Character, TrieNode> children = new HashMap<>();
    String word = null;
}

public class Solution {
    private int ROWS, COLS;
    private char[][] board;
    private List<String> res = new ArrayList<>();

    public List<String> findWords(char[][] board, String[] words) {
        this.board = board;
        ROWS = board.length;
        COLS = board[0].length;

        TrieNode root = new TrieNode();
        for (String word : words) {
            TrieNode node = root;
            for (char c : word.toCharArray()) {
                node.children.putIfAbsent(c, new TrieNode());
                node = node.children.get(c);
            }
            node.word = word;
        }

        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                backtrack(r, c, root);
            }
        }
        return res;
    }

    private void backtrack(int r, int c, TrieNode parent) {
        char letter = board[r][c];
        TrieNode currNode = parent.children.get(letter);
        if (currNode == null) return;

        if (currNode.word != null) {
            res.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 < ROWS && nc >= 0 && nc < COLS && board[nr][nc] != '#') {
                backtrack(nr, nc, currNode);
            }
        }
        board[r][c] = letter;

        if (currNode.children.isEmpty()) {
            parent.children.remove(letter);
        }
    }

    // Example usage
    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"};
        Solution sol = new Solution();
        System.out.println(sol.findWords(board, words));
    }
}
Line Notes
board[r][c] = '#';Mark cell visited in-place to save space instead of extra visited array.
board[r][c] = letter;Restore cell letter after recursion to maintain board state.
if (currNode.word != null) {Add found word to results and mark null to avoid duplicates.
if (currNode.children.isEmpty()) {Prune trie nodes with no children to optimize search space.
for (int r = 0; r < ROWS; r++) {Start backtracking from every cell in the board.
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
using namespace std;

struct TrieNode {
    unordered_map<char, TrieNode*> children;
    string word = "";
};

class Solution {
    int ROWS, COLS;
    vector<vector<char>> board;
    vector<string> res;

    void insertWord(TrieNode* root, const string& word) {
        TrieNode* node = root;
        for (char c : word) {
            if (!node->children.count(c)) {
                node->children[c] = new TrieNode();
            }
            node = node->children[c];
        }
        node->word = word;
    }

    void backtrack(int r, int c, TrieNode* parent) {
        char letter = board[r][c];
        if (!parent->children.count(letter)) return;
        TrieNode* currNode = parent->children[letter];

        if (!currNode->word.empty()) {
            res.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 < ROWS && nc >= 0 && nc < COLS && board[nr][nc] != '#') {
                backtrack(nr, nc, currNode);
            }
        }
        board[r][c] = letter;

        if (currNode->children.empty()) {
            parent->children.erase(letter);
            delete currNode;
        }
    }

public:
    vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
        this->board = board;
        ROWS = board.size();
        COLS = board[0].size();
        TrieNode* root = new TrieNode();
        for (const string& word : words) {
            insertWord(root, word);
        }

        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                backtrack(r, c, root);
            }
        }
        return res;
    }
};

// Example usage
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> result = sol.findWords(board, words);
    for (const string& w : result) {
        cout << w << " ";
    }
    cout << endl;
    return 0;
}
Line Notes
board[r][c] = '#';Mark cell visited in-place to save space instead of extra visited matrix.
board[r][c] = letter;Restore cell letter after recursion to maintain board state.
if (!currNode->word.empty()) {Add found word to results and clear to avoid duplicates.
if (currNode->children.empty()) {Prune trie nodes with no children to optimize search space and free memory.
for (int r = 0; r < ROWS; r++) {Start backtracking from every cell in the board.
class TrieNode {
    constructor() {
        this.children = new Map();
        this.word = null;
    }
}

var findWords = function(board, words) {
    const root = new TrieNode();
    for (const word of words) {
        let node = root;
        for (const char of word) {
            if (!node.children.has(char)) {
                node.children.set(char, new TrieNode());
            }
            node = node.children.get(char);
        }
        node.word = word;
    }

    const ROWS = board.length, COLS = board[0].length;
    const res = [];

    function backtrack(r, c, parent) {
        const letter = board[r][c];
        const currNode = parent.children.get(letter);
        if (!currNode) return;

        if (currNode.word !== null) {
            res.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) {
            parent.children.delete(letter);
        }
    }

    for (let r = 0; r < ROWS; r++) {
        for (let c = 0; c < COLS; c++) {
            backtrack(r, c, root);
        }
    }
    return res;
};

// Example usage
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));
Line Notes
board[r][c] = '#';Mark cell visited in-place to save space instead of extra visited set.
board[r][c] = letter;Restore cell letter after recursion to maintain board state.
if (currNode.word !== null) {Add found word to results and mark null to avoid duplicates.
if (currNode.children.size === 0) {Prune trie nodes with no children to optimize search space.
for (let r = 0; r < ROWS; r++) {Start backtracking from every cell in the board.
⏱
Complexity
TimeO(N*M*4^L) similar to approach 2
SpaceO(W*L) for trie, but no extra visited set due to in-place marking

In-place marking saves space, and pruning trie nodes reduces search space dynamically, improving runtime in practice.

💡 This approach is the most memory efficient while maintaining the same time complexity as the trie backtracking method.
Interview Verdict: Accepted

This is the preferred approach in interviews when asked about space optimization or in-place modifications.

📊
All Approaches - One-Glance Tradeoffs
💡 In most interviews, approach 2 or 3 (trie with backtracking) is the best to code. Brute force is only for initial discussion.
ApproachTimeSpaceStack RiskReconstructUse In Interview
1. Brute ForceO(W * N * M * 4^L)O(L) recursion + O(L) visited setYes (deep recursion for long words)N/AMention only - never code due to inefficiency
2. Backtracking with TrieO(N * M * 4^L)O(W * L) trie + O(L) recursionYesN/ACode this for efficient solution
3. Backtracking with Trie and In-Place MarkingO(N * M * 4^L)O(W * L) trie, no extra visited setYesN/ACode this if asked for space optimization
💼
Interview Strategy
💡 Use this guide to understand the problem deeply, starting from brute force to optimized solutions. Practice explaining each approach clearly and know when to use each.

How to Present

Step 1: Clarify problem constraints and inputs.Step 2: Describe brute force approach and its limitations.Step 3: Introduce trie to optimize prefix checking and pruning.Step 4: Explain in-place marking and trie pruning for space optimization.Step 5: Code the optimized solution and test with examples.

Time Allocation

Clarify: 5min → Approach discussion: 10min → Coding: 20min → Testing & optimization: 10min. Total ~45min

What the Interviewer Tests

Interviewer tests your ability to handle complex backtracking, use advanced data structures like tries, optimize time and space, and write clean, bug-free code.

Common Follow-ups

  • How to handle very large word lists? → Use trie and pruning aggressively.
  • Can you optimize space usage? → Use in-place board marking and prune trie nodes.
  • What if diagonal moves are allowed? → Modify neighbor exploration accordingly.
  • How to avoid duplicates in results? → Mark found words as null in trie.
💡 These follow-ups test your depth of understanding and ability to adapt the solution to variations and constraints.
🔍
Pattern Recognition

When to Use

1) Need to find multiple words in a grid; 2) Words share prefixes; 3) Need to avoid repeated searches; 4) Backtracking with pruning is possible

Signature Phrases

find all wordsadjacent cellsno cell reusedlist of words

NOT This Pattern When

Single word search without multiple words or prefix pruning is simpler and not this pattern.

Similar Problems

Word Search I - simpler single word search in gridBoggle Game - similar grid word search with dictionaryPrefix Tree (Trie) Problems - foundational for prefix pruning

Practice

(1/5)
1. Given a binary tree where each node contains an integer value, you need to find the maximum sum of values along any path. The path can start and end at any node, and must be continuous (parent-child connections). Which approach guarantees finding the optimal maximum path sum efficiently?
easy
A. Use a recursive postorder traversal that computes maximum gains from children and updates a global max path sum including both children at each node.
B. Use a greedy traversal that picks the maximum child sum at each node without backtracking.
C. Use a breadth-first search to accumulate sums level by level and pick the maximum sum found.
D. Use dynamic programming on node values treating the tree as a linear array and summing consecutive nodes.

Solution

  1. Step 1: Understand the problem requires paths that can bend at any node, including both children.

    A greedy approach that only picks one child at each node misses paths that include both children and the node itself.
  2. Step 2: Recognize that a recursive postorder traversal can compute max gains from left and right subtrees, and update a global max including both children plus current node.

    This approach ensures all possible paths are considered and the global max is updated correctly.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Greedy and BFS approaches fail to consider paths bending at nodes [OK]
Hint: Optimal solution uses recursion with global max tracking [OK]
Common Mistakes:
  • Greedy picking max child only
  • Treating tree as linear array
  • Using BFS for path sums
2. Given a list of unique words, you need to find all pairs of distinct indices (i, j) such that the concatenation of words[i] + words[j] forms a palindrome. Which approach guarantees an optimal solution in terms of time complexity for large inputs?
easy
A. Brute force nested loops checking all pairs and verifying palindrome by concatenation
B. Building a Trie of reversed words and tracking palindrome suffixes to find pairs efficiently
C. Dynamic programming to find palindromic substrings and then pairing indices accordingly
D. Greedy approach by sorting words and pairing those with matching prefixes and suffixes

Solution

  1. Step 1: Understand brute force limitations

    Brute force checks all pairs, resulting in O(n^2 * k) time, which is inefficient for large n.
  2. Step 2: Recognize Trie-based optimization

    Building a Trie of reversed words allows prefix matching and palindrome suffix tracking, reducing redundant checks and achieving O(n * k^2) time.
  3. Final Answer:

    Option B -> Option B
  4. Quick Check:

    Trie-based approach is known optimal for palindrome pairs [OK]
Hint: Trie with palindrome suffixes optimizes palindrome pair search [OK]
Common Mistakes:
  • Assuming brute force is acceptable for large inputs
  • Confusing palindrome substring DP with pair finding
  • Believing greedy prefix-suffix matching suffices
3. Consider the following buggy LFUCache code snippet. Which line contains the subtle bug that causes incorrect eviction behavior?
def put(self, key: int, value: int) -> None:
    if self.capacity == 0:
        return
    self.time += 1
    if key in self.cache:
        _, freq, _ = self.cache[key]
        self.cache[key] = (value, freq + 1, self.time)
        return
    if len(self.cache) == self.capacity:
        min_freq = min(freq for _, freq, _ in self.cache.values())
        lfu_keys = [k for k, (_, freq, _) in self.cache.items() if freq == min_freq]
        lru_key = min(lfu_keys, key=lambda k: self.cache[k][2])
        del self.cache[lru_key]
    self.cache[key] = (value, 1, self.time)
medium
A. Missing update of minimum frequency after eviction
B. Line: self.cache[key] = (value, freq + 1, self.time) when key exists
C. Line: min_freq = min(freq for _, freq, _ in self.cache.values())
D. Line: self.time += 1

Solution

  1. Step 1: Identify eviction and frequency update

    The code evicts the LFU and LRU key correctly but does not update any variable tracking the minimum frequency after eviction.
  2. Step 2: Understand impact of missing minFreq update

    Without updating minFreq, subsequent evictions or frequency increments may behave incorrectly, evicting wrong keys.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Missing minFreq update causes eviction errors [OK]
Hint: Eviction needs minFreq update to maintain correctness [OK]
Common Mistakes:
  • Assuming eviction alone suffices
  • Updating frequency but not minFreq
  • Confusing time increment with frequency update
4. What is the time complexity of the optimized approach that uses sorting, offline queries, and a bitwise trie for the Maximum XOR With Element From Array problem, given n elements in nums and q queries, assuming 32-bit integers?
medium
A. O((n + q) * log(maxNum))
B. O((n + q) * 32)
C. O(n * log(maxNum) * q)
D. O(n * q)

Solution

  1. Step 1: Analyze sorting and offline queries

    Sorting nums and queries takes O(n log n + q log q), dominated by O((n + q) log (n + q)) which is less than main complexity.
  2. Step 2: Analyze trie operations

    Each insertion and query takes O(log(maxNum)) = O(32) for 32-bit integers. Total insertions = n, total queries = q.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Combined complexity is O((n + q) * log(maxNum)) [OK]
Hint: Trie ops per element and query scale with log(maxNum) [OK]
Common Mistakes:
  • Confusing n*q brute force with optimized approach
  • Ignoring log(maxNum) factor
  • Assuming linear time per query
5. Suppose the problem is modified so that elements from the lists can be reused multiple times in the range (i.e., the range must cover at least one element from each list, but elements can be counted multiple times). Which modification to the optimal two-pointer approach is necessary to correctly solve this variant?
hard
A. Use a min-heap to always pick the smallest current element from each list and update the maximum accordingly.
B. No modification needed; the original two-pointer approach works as is.
C. Track counts of elements but do not decrement counts when moving the left pointer to allow reuse.
D. Enumerate all combinations with replacement and select the minimal range.

Solution

  1. Step 1: Understand reuse impact

    Allowing reuse means elements can be counted multiple times, so sliding window coverage logic breaks.
  2. Step 2: Identify suitable approach

    Min-heap approach maintains current smallest elements from each list and updates max to find minimal range, handling reuse correctly.
  3. Final Answer:

    Option A -> Option A
  4. Quick Check:

    Min-heap approach is standard for reuse variant [OK]
Hint: Reuse breaks sliding window; min-heap handles dynamic coverage [OK]
Common Mistakes:
  • Assuming no change needed
  • Trying to fix counts without changing approach
  • Using brute force for large inputs
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard