Guide contents

Lesson 132 of 250

79. Word Search

NeetCode: Word Search · LeetCode: 79. Word Search · Difficulty: Medium

Given an m x n grid of characters board and a string word, return true if word exists in the grid. The word is built from letters of sequentially adjacent cells (up, down, left, right), and the same cell may not be used more than once.

Example

Text
board = [["A","B","C","E"],
         ["S","F","C","S"],
         ["A","D","E","E"]]

word = "ABCCED"    Output: true
word = "SEE"       Output: true
word = "ABCB"      Output: false

Brute force: DFS with a list of visited cells

Try every cell as the start. From a cell that matches the current letter, recurse into the four neighbours for the next letter. To respect "no reuse", keep the current path as a list of cell numbers (r * cols + c) and check path.contains(..) before entering a cell. Every visit pays an O(L) scan of the path, and the search continues even when the board cannot contain the word.

LeetCode's Java signature is boolean exist(char[][] board, String word). The path is a List<Integer>: contains compares boxed values with equals, so it finds a cell number by value. (A List<int[]> would not work, because arrays compare by identity.) The grid helper in main builds a char[][] from row strings.

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

public class Main {
    static class Solution {
        private static final int[][] DIRS = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

        public boolean exist(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, 0, r, c, new ArrayList<>())) {
                        return true;
                    }
                }
            }
            return false;
        }

        private boolean dfs(char[][] board, String word, int idx, int r, int c, List<Integer> path) {
            if (idx == word.length()) {
                return true;
            }
            if (r < 0 || c < 0 || r >= board.length || c >= board[0].length) {
                return false;
            }
            Integer cell = r * board[0].length + c;
            if (board[r][c] != word.charAt(idx) || path.contains(cell)) {
                return false;
            }
            path.add(cell);
            for (int[] d : DIRS) {
                if (dfs(board, word, idx + 1, r + d[0], c + d[1], path)) {
                    return true;
                }
            }
            path.remove(path.size() - 1);
            return false;
        }
    }

    static char[][] grid(String... rows) {
        char[][] g = new char[rows.length][];
        for (int i = 0; i < rows.length; i++) {
            g[i] = rows[i].toCharArray();
        }
        return g;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        char[][] board = grid("ABCE", "SFCS", "ADEE");
        System.out.println(s.exist(board, "ABCCED"));
        System.out.println(s.exist(board, "SEE"));
        System.out.println(s.exist(board, "ABCB"));
        System.out.println(s.exist(grid("A"), "A"));
        System.out.println(s.exist(grid("A"), "AB"));
    }
}
Output
true
true
false
true
false

Time: O(m · n · 3^L · L) where L is the word length: after the first letter each step has at most 3 new directions, and the path check costs up to L. Space: O(L).

Optimal: mark cells in place, prune early

Both versions explore the same tree, so the worst case stays O(m · n · 3^L); the second removes the overhead and skips hopeless searches:

  • Mark in place. Overwrite the current cell with a sentinel ('#') while exploring below it and restore it afterwards. The "already used" test becomes a plain character comparison, O(1), with no path list. The board is the caller's array, so the method changes it temporarily and every path out of dfs restores it.
  • Cheap rejections up front. If the word is longer than the board has cells, or the board has too few of some letter, the answer is false without any search. The letter counts use an int[128] indexed by character code, which covers the ASCII letters in the constraints.
  • Search from the rarer end. If the first letter of the word is more common on the board than the last, reverse the word. Fewer starting cells means fewer DFS roots, and reversing the word does not change whether a path exists.
Java
public class Main {
    static class Solution {
        public boolean exist(char[][] board, String word) {
            int rows = board.length;
            int cols = board[0].length;
            if (word.length() > rows * cols) {
                return false;
            }
            int[] counts = new int[128];
            for (char[] row : board) {
                for (char ch : row) {
                    counts[ch]++;
                }
            }
            int[] needed = new int[128];
            for (char ch : word.toCharArray()) {
                needed[ch]++;
            }
            for (int ch = 0; ch < 128; ch++) {
                if (counts[ch] < needed[ch]) {
                    return false;
                }
            }
            char[] w = word.toCharArray();
            if (counts[w[0]] > counts[w[w.length - 1]]) {
                reverse(w);
            }
            for (int r = 0; r < rows; r++) {
                for (int c = 0; c < cols; c++) {
                    if (dfs(board, w, 0, r, c)) {
                        return true;
                    }
                }
            }
            return false;
        }

        private void reverse(char[] w) {
            for (int i = 0, j = w.length - 1; i < j; i++, j--) {
                char t = w[i];
                w[i] = w[j];
                w[j] = t;
            }
        }

        private boolean dfs(char[][] board, char[] word, int idx, int r, int c) {
            if (board[r][c] != word[idx]) {
                return false;
            }
            if (idx + 1 == word.length) {
                return true;
            }
            char saved = board[r][c];
            board[r][c] = '#'; // mark as used
            int rows = board.length;
            int cols = board[0].length;
            boolean found = (r > 0 && dfs(board, word, idx + 1, r - 1, c))
                    || (r + 1 < rows && dfs(board, word, idx + 1, r + 1, c))
                    || (c > 0 && dfs(board, word, idx + 1, r, c - 1))
                    || (c + 1 < cols && dfs(board, word, idx + 1, r, c + 1));
            board[r][c] = saved; // undo
            return found;
        }
    }

    static char[][] grid(String... rows) {
        char[][] g = new char[rows.length][];
        for (int i = 0; i < rows.length; i++) {
            g[i] = rows[i].toCharArray();
        }
        return g;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        char[][] board = grid("ABCE", "SFCS", "ADEE");
        System.out.println(s.exist(board, "ABCCED"));
        System.out.println(s.exist(board, "SEE"));
        System.out.println(s.exist(board, "ABCB"));
        System.out.println(s.exist(grid("A"), "A"));
        System.out.println(s.exist(grid("A"), "AB"));
    }
}
Output
true
true
false
true
false

Time: O(m · n · 3^L) worst case. Space: O(L) recursion depth; the board is mutated in place and restored. The bounds checks use r > 0 and r + 1 < rows before each recursive call, so no index is ever outside the board.