150NeetCode 150

63.Word Search II

HardBlind 75Tries · 3/3

Problem

Given an m x n grid of letters board and a list of strings words, return all words from the list that can be formed on the board.

A word is formed from a path of horizontally or vertically adjacent cells, and the same cell may not be used more than once within a single word. Each found word appears once in the output, in any order.

Examples

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"]
Explanation: "oath" goes o(0,0) a(0,1) t(1,1) h(2,1); "eat" goes e(1,3) a(1,2) t(1,1).
Input:  board = [["a","b"],["c","d"]], words = ["abcb"]
Output: []
Explanation: Reaching the second 'b' would require reusing cell (0,1).

Constraints

  • m == board.length, n == board[i].length
  • 1 <= m, n <= 12
  • 1 <= words.length <= 3 * 10^4
  • 1 <= words[i].length <= 10
  • board[i][j] and words[i] consist of lowercase English letters; all words[i] are unique.

💡 Key Insight

Running a separate DFS per word repeats the same grid exploration thousands of times. Instead, put all words in a trie and run one DFS per starting cell that walks the board and the trie together: a path is abandoned the moment its letters are not a prefix of any word. Storing the full word at its terminal node, clearing it once found, and pruning emptied trie branches keeps the search from revisiting exhausted prefixes.

Brute Force (Word Search per Word)

SpaceO(L) recursion depth.

Intuition

Solve "Word Search I" for each word independently: try every cell as a start and backtrack along matching letters. Stop at the first successful placement of a word.

Algorithm

  1. For each word in words:
  2. For every cell (r, c), run dfs(r, c, 0); if it succeeds, add word and move to the next word.
  3. dfs(r, c, i): if i == word.length() return true; return false if out of bounds or board[r][c] != word[i].
  4. Mark the cell '#', recurse into 4 neighbours with i + 1, then restore the letter.

Visualization

word = "eat"
start (1,3)='e' -> (1,2)='a' -> (1,1)='t' -> i == 3 -> found

word = "rain"
start (2,3)='r' -> neighbours: e, k, v (no 'a') -> fail; no other 'r' -> not found

Code

java
import java.util.ArrayList;
import java.util.List;

class Solution {
    public List<String> findWords(char[][] board, String[] words) {
        List<String> result = new ArrayList<>();
        for (String word : words) {
            if (exists(board, word)) {
                result.add(word);
            }
        }
        return result;
    }

    private boolean exists(char[][] board, String word) {
        for (int r = 0; r < board.length; r++) {
            for (int c = 0; c < board[0].length; c++) {
                if (dfs(board, word, r, c, 0)) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean dfs(char[][] board, String word, int r, int c, int i) {
        if (i == word.length()) {
            return true;
        }
        if (r < 0 || c < 0 || r >= board.length || c >= board[0].length || board[r][c] != word.charAt(i)) {
            return false;
        }
        char saved = board[r][c];
        board[r][c] = '#'; // mark visited for this path
        boolean found = dfs(board, word, r + 1, c, i + 1) || dfs(board, word, r - 1, c, i + 1)
                || dfs(board, word, r, c + 1, i + 1) || dfs(board, word, r, c - 1, i + 1);
        board[r][c] = saved; // backtrack
        return found;
    }
}

Complexity

  • Time: O(W · m · n · 4 · 3^(L-1)) — for each of W words, each cell may start a DFS that branches up to 3 ways for L steps. Too slow for W = 3 · 10^4.
  • Space: O(L) recursion depth.

Complexity Comparison

Approach Time Space
Word Search per Word O(W · m · n · 3^L) O(L)
Trie + Backtracking O(m · n · 3^L) O(total chars)
Trie + Pruning O(m · n · 3^L), far fewer visits in practice O(total chars)

Common Pitfalls

  • Adding the same word twice when it can be formed from several starting cells — clear node.word after recording it (or use a set).
  • Returning after finding a word: a found word may be a prefix of another ("oa" and "oath"), so keep exploring.
  • Forgetting to restore board[r][c] after the recursive calls, which corrupts later searches.
  • Checking node.children[ch - 'a'] before checking for the '#' marker gives a negative index.