Word Search II

hard

Given an m x n board of characters and a list of strings words, return all words that can be formed by sequentially adjacent cells on the board. Adjacent cells are horizontally or vertically neighboring. The same cell may not be used more than once in a single word.

Examples

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"]
  • Explanation: The words "eat" and "oath" can be formed from adjacent cells on the board.

Example 2

  • Input: board = [["a","b"],["c","d"]], words = ["abcb"]
  • Output: []
  • Explanation: The word "abcb" would require reusing a cell, which is not allowed.

Constraints

  • m == board.length
  • n == board[i].length
  • 1 <= m, n <= 12
  • board[i][j] is a lowercase English letter.
  • 1 <= words.length <= 3 * 10^4
  • 1 <= words[i].length <= 10
  • words[i] consists of lowercase English letters.
  • All strings in words are unique.

Approaches

2 worked solutions ranked from straightforward to optimal — each with intuition, algorithm, complexity, and verified code in C++, Java, Python, and Go.

Approach 1

Brute Force Backtracking per Word

Intuition

For each word in the list, we can perform a DFS/backtracking search on the board starting from every cell. For each starting position, we try to match the word character by character, moving to adjacent cells and marking visited cells to avoid reuse. This straightforward approach checks each word independently, which leads to a lot of repeated traversal when many words share common prefixes.

Algorithm

  1. For each word in the word list, iterate over all cells on the board as potential starting positions.
  2. From each starting cell, perform a DFS: if the current cell matches the current character of the word, mark the cell as visited and recurse in all four directions for the next character.
  3. If all characters are matched, add the word to the result.
  4. Backtrack by restoring the cell's original value.
  5. Return all found words.
TimeO(w * m * n * 4^l) where w is the number of words, m*n is the board size, and l is the max word lengthSpaceO(l) for the recursion stack

Code (C++ · Java · Python · Go)

C++17
class Solution {
    int dirs[4][2] = {{0,1},{0,-1},{1,0},{-1,0}};

    bool dfs(vector<vector<char>>& board, const string& word, int i, int j, int k) {
        if (k == word.size()) return true;
        if (i < 0 || i >= board.size() || j < 0 || j >= board[0].size()) return false;
        if (board[i][j] != word[k]) return false;

        char temp = board[i][j];
        board[i][j] = '#';

        for (auto& d : dirs) {
            if (dfs(board, word, i + d[0], j + d[1], k + 1)) {
                board[i][j] = temp;
                return true;
            }
        }

        board[i][j] = temp;
        return false;
    }

public:
    vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
        vector<string> result;
        for (const string& word : words) {
            bool found = false;
            for (int i = 0; i < board.size() && !found; i++) {
                for (int j = 0; j < board[0].size() && !found; j++) {
                    if (dfs(board, word, i, j, 0)) {
                        result.push_back(word);
                        found = true;
                    }
                }
            }
        }
        return result;
    }
};
Java 21
class Solution {
    private int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}};

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

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

    private boolean dfs(char[][] board, String word, int i, int j, int k) {
        if (k == word.length()) return true;
        if (i < 0 || i >= board.length || j < 0 || j >= board[0].length) return false;
        if (board[i][j] != word.charAt(k)) return false;

        char temp = board[i][j];
        board[i][j] = '#';

        for (int[] d : dirs) {
            if (dfs(board, word, i + d[0], j + d[1], k + 1)) {
                board[i][j] = temp;
                return true;
            }
        }

        board[i][j] = temp;
        return false;
    }
}
Python 3.12
from typing import List


class Solution:
    def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
        m, n = len(board), len(board[0])
        result = []

        def dfs(i: int, j: int, k: int, word: str) -> bool:
            if k == len(word):
                return True
            if i < 0 or i >= m or j < 0 or j >= n or board[i][j] != word[k]:
                return False

            temp = board[i][j]
            board[i][j] = "#"

            for di, dj in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
                if dfs(i + di, j + dj, k + 1, word):
                    board[i][j] = temp
                    return True

            board[i][j] = temp
            return False

        for word in words:
            found = False
            for i in range(m):
                for j in range(n):
                    if dfs(i, j, 0, word):
                        result.append(word)
                        found = True
                        break
                if found:
                    break

        return result
Go 1.26
func findWords(board [][]byte, words []string) []string {
    dirs := [][2]int{{0, 1}, {0, -1}, {1, 0}, {-1, 0}}
    m, n := len(board), len(board[0])
    result := []string{}

    var dfs func(i, j, k int, word string) bool
    dfs = func(i, j, k int, word string) bool {
        if k == len(word) {
            return true
        }
        if i < 0 || i >= m || j < 0 || j >= n || board[i][j] != word[k] {
            return false
        }

        temp := board[i][j]
        board[i][j] = '#'

        for _, d := range dirs {
            if dfs(i+d[0], j+d[1], k+1, word) {
                board[i][j] = temp
                return true
            }
        }

        board[i][j] = temp
        return false
    }

    for _, word := range words {
        found := false
        for i := 0; i < m && !found; i++ {
            for j := 0; j < n && !found; j++ {
                if dfs(i, j, 0, word) {
                    result = append(result, word)
                    found = true
                }
            }
        }
    }

    return result
}
Approach 2

Trie + Backtracking

Intuition

Instead of searching for each word independently, we build a trie from all the words and perform a single DFS traversal of the board. At each cell, we check if the current path through the board corresponds to a prefix in the trie. If not, we prune the search immediately. When we reach a trie node marked as a word ending, we add that word to the results. We also remove completed words from the trie to avoid finding duplicates and to enable further pruning of empty branches.

Algorithm

  1. Build a trie from all words in the list. Store the complete word at each terminal node.
  2. For each cell on the board, if the character matches a child of the trie root, start a DFS.
  3. During DFS, move to the corresponding trie child. If no child exists for the current character, prune.
  4. If the current trie node marks a word ending, add the word to the results and clear the marker to avoid duplicates.
  5. Mark the board cell as visited, recurse in all four directions, then restore the cell.
  6. Optimization: after exploring all children of a trie node, if it has no remaining children, remove it from the parent to prune future searches.
  7. Return the result list.
TimeO(m * n * 4^l) where l is the max word length, but pruning makes this much faster in practiceSpaceO(w * l) for the trie, where w is the number of words

Code (C++ · Java · Python · Go)

C++17
class Solution {
    struct TrieNode {
        TrieNode* children[26] = {};
        string word;
    };

    int dirs[4][2] = {{0,1},{0,-1},{1,0},{-1,0}};

    void dfs(vector<vector<char>>& board, int i, int j, TrieNode* node, vector<string>& result) {
        char c = board[i][j];
        if (c == '#' || !node->children[c - 'a']) return;

        node = node->children[c - 'a'];

        if (!node->word.empty()) {
            result.push_back(node->word);
            node->word.clear();
        }

        board[i][j] = '#';

        for (auto& d : dirs) {
            int ni = i + d[0], nj = j + d[1];
            if (ni >= 0 && ni < board.size() && nj >= 0 && nj < board[0].size()) {
                dfs(board, ni, nj, node, result);
            }
        }

        board[i][j] = c;

        // Prune empty branches
        bool hasChildren = false;
        for (int k = 0; k < 26; k++) {
            if (node->children[k]) {
                hasChildren = true;
                break;
            }
        }
        if (!hasChildren && node->word.empty()) {
            // Find and remove this node from parent (handled by not pruning parent here for simplicity)
        }
    }

public:
    vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
        TrieNode* root = new TrieNode();

        for (const string& word : words) {
            TrieNode* node = root;
            for (char c : word) {
                int idx = c - 'a';
                if (!node->children[idx]) {
                    node->children[idx] = new TrieNode();
                }
                node = node->children[idx];
            }
            node->word = word;
        }

        vector<string> result;
        for (int i = 0; i < board.size(); i++) {
            for (int j = 0; j < board[0].size(); j++) {
                dfs(board, i, j, root, result);
            }
        }

        return result;
    }
};
Java 21
class Solution {
    private int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}};

    private class TrieNode {
        TrieNode[] children = new TrieNode[26];
        String word = null;
    }

    public List<String> findWords(char[][] board, String[] words) {
        TrieNode root = new TrieNode();

        for (String word : words) {
            TrieNode node = root;
            for (char c : word.toCharArray()) {
                int idx = c - 'a';
                if (node.children[idx] == null) {
                    node.children[idx] = new TrieNode();
                }
                node = node.children[idx];
            }
            node.word = word;
        }

        List<String> result = new ArrayList<>();
        for (int i = 0; i < board.length; i++) {
            for (int j = 0; j < board[0].length; j++) {
                dfs(board, i, j, root, result);
            }
        }

        return result;
    }

    private void dfs(char[][] board, int i, int j, TrieNode node, List<String> result) {
        char c = board[i][j];
        if (c == '#' || node.children[c - 'a'] == null) return;

        node = node.children[c - 'a'];

        if (node.word != null) {
            result.add(node.word);
            node.word = null;
        }

        board[i][j] = '#';

        for (int[] d : dirs) {
            int ni = i + d[0], nj = j + d[1];
            if (ni >= 0 && ni < board.length && nj >= 0 && nj < board[0].length) {
                dfs(board, ni, nj, node, result);
            }
        }

        board[i][j] = c;
    }
}
Python 3.12
from typing import List


class Solution:
    def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
        root = {}

        for word in words:
            node = root
            for c in word:
                if c not in node:
                    node[c] = {}
                node = node[c]
            node["#"] = word

        m, n = len(board), len(board[0])
        result = []

        def dfs(i: int, j: int, node: dict) -> None:
            c = board[i][j]
            if c not in node:
                return

            next_node = node[c]

            if "#" in next_node:
                result.append(next_node["#"])
                del next_node["#"]

            board[i][j] = "."

            for di, dj in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
                ni, nj = i + di, j + dj
                if 0 <= ni < m and 0 <= nj < n and board[ni][nj] != ".":
                    dfs(ni, nj, next_node)

            board[i][j] = c

            if not next_node:
                del node[c]

        for i in range(m):
            for j in range(n):
                dfs(i, j, root)

        return result
Go 1.26
type TrieNode struct {
    children [26]*TrieNode
    word     string
}

func findWords(board [][]byte, words []string) []string {
    root := &TrieNode{}

    for _, word := range words {
        node := root
        for _, c := range word {
            idx := c - 'a'
            if node.children[idx] == nil {
                node.children[idx] = &TrieNode{}
            }
            node = node.children[idx]
        }
        node.word = word
    }

    dirs := [][2]int{{0, 1}, {0, -1}, {1, 0}, {-1, 0}}
    m, n := len(board), len(board[0])
    result := []string{}

    var dfs func(i, j int, node *TrieNode)
    dfs = func(i, j int, node *TrieNode) {
        c := board[i][j]
        if c == '#' || node.children[c-'a'] == nil {
            return
        }

        node = node.children[c-'a']

        if node.word != "" {
            result = append(result, node.word)
            node.word = ""
        }

        board[i][j] = '#'

        for _, d := range dirs {
            ni, nj := i+d[0], j+d[1]
            if ni >= 0 && ni < m && nj >= 0 && nj < n {
                dfs(ni, nj, node)
            }
        }

        board[i][j] = c
    }

    for i := 0; i < m; i++ {
        for j := 0; j < n; j++ {
            dfs(i, j, root)
        }
    }

    return result
}

Frequently asked questions

What is the optimal time complexity of Word Search II?

The most efficient approach in this guide ("Trie + Backtracking") runs in O(m * n * 4^l) where l is the max word length, but pruning makes this much faster in practice time and uses O(w * l) for the trie, where w is the number of words extra space. Earlier brute-force approaches are slower; see all 2 approaches above for the full progression.

What pattern does Word Search II use?

Word Search II is a hard-level Tries problem involving Array, String, Backtracking. Recognizing the pattern is half the work — see the intuition section above for how to spot it.

Can Word Search II be solved without extra space?

The most space-efficient approach in this guide uses O(w * l) for the trie, where w is the number of words extra space (excluding the input). If you're aiming for in-place — see the trade-off in the algorithm section above — the brute-force approach uses O(l) for the recursion stack, while the optimized version uses O(w * l) for the trie, where w is the number of words.

Which programming languages does this Ratta solution support?

Every approach above ships with verified, runnable solutions in C++, Java, Python, and Go. The starter templates in the workspace match the same four languages so you can practice in your interview language of choice.

Related Problems

Original problem: leetcode.com

Loading editor...
board=
[["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]]
words=
["oath","pea","eat","rain"]
["oath","eat"]