79. Word Search - Explanation

Problem Link

Description

Given a 2-D grid of characters board and a string word, return true if the word is present in the grid, otherwise return false.

For the word to be present it must be possible to form it with a path in the board with horizontally or vertically neighboring cells. The same cell may not be used more than once in a word.

Example 1:

Input: 
board = [
  ["A","B","C","D"],
  ["S","A","A","T"],
  ["A","C","A","E"]
],
word = "CAT"

Output: true

Example 2:

Input: 
board = [
  ["A","B","C","D"],
  ["S","A","A","T"],
  ["A","C","A","E"]
],
word = "BAT"

Output: false

Constraints:

  • 1 <= board.length, board[i].length <= 5
  • 1 <= word.length <= 10
  • board and word consists of only lowercase and uppercase English letters.


Topics

Recommended Time & Space Complexity

You should aim for a solution with O(m * (4^n)) time and O(n) space, where m is the number of cells in the given board and n is the size of the given word.


Hint 1

As we can start at any cell on the board, we can explore all paths from that cell. Can you think of an algorithm to do so? Also, you should consider a way to avoid visiting a cell that has already been visited in the current path.


Hint 2

We can use a hash set to avoid revisiting a cell in the current path by inserting the (row, col) of the visiting cell into the hash set and exploring all paths (four directions, as we can move to four neighboring cells) from that cell. Can you think of the base condition for this recursive path? Maybe you should consider the board boundaries, and also, we can extend a path if the character at the cell matches the character in the word.


Hint 3

We can use backtracking, starting from each cell on the board with coordinates (row, col) and index i for the given word. We return false if (row, col) is out of bounds or if board[row][col] != word[i]. When i reaches the end of the word, we return true, indicating a valid path. At each step, we add (row, col) to a hash set to avoid revisiting cells. After exploring the four possible directions, we backtrack and remove (row, col) from the hash set.


Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Backtracking - Exploring all possible paths by making choices, recursing, and undoing choices to try alternatives
  • Depth-First Search (DFS) - Traversing a graph or grid by exploring as far as possible along each branch before backtracking
  • 2D Grid Traversal - Moving through a matrix in four directions while tracking visited cells to avoid revisiting
  • Recursion - Understanding recursive function calls and base cases for termination

1. Backtracking (Hash Set)

Intuition

We need to check if the word can be formed by walking up/down/left/right on the grid, using each cell at most once in the same path.

So for every cell, we try to start the word there:

  • If the current cell matches the current character, we move to its 4 neighbors for the next character.
  • While exploring, we mark the cell as visited (in a hash set) so we don't reuse it in the same path.
  • If a path fails, we undo (backtrack) the visit and try other directions.

If we ever match all characters, we return true (found the word).

Algorithm

  1. For each cell in the grid, attempt to start matching word from that cell.
  2. Use DFS with (row, col, i) where i is the index in word we need to match.
  3. In DFS:
    • If i == len(word), all characters matched → return true.
    • If out of bounds, mismatch, or already visited → return false.
    • Mark (row, col) as visited.
    • Recurse to 4 neighbors with i + 1.
    • Unmark (row, col) (backtrack).
  4. If any start cell returns true, answer is true; otherwise false.
class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        ROWS, COLS = len(board), len(board[0])
        path = set()

        def dfs(r, c, i):
            if i == len(word):
                return True

            if (min(r, c) < 0 or
                r >= ROWS or c >= COLS or
                word[i] != board[r][c] or
                (r, c) in path):
                return False

            path.add((r, c))
            res = (dfs(r + 1, c, i + 1) or
                   dfs(r - 1, c, i + 1) or
                   dfs(r, c + 1, i + 1) or
                   dfs(r, c - 1, i + 1))
            path.remove((r, c))
            return res

        for r in range(ROWS):
            for c in range(COLS):
                if dfs(r, c, 0):
                    return True
        return False

Time & Space Complexity

  • Time complexity: O(m4n)O(m * 4 ^ n)
  • Space complexity: O(n)O(n)

Where mm is the number of cells in the boardboard and nn is the length of the wordword.


2. Backtracking (Visited Array)

Intuition

We try to form the word by walking through adjacent cells (up, down, left, right) in the grid.
Each cell can be used only once in the current path, so we keep a visited matrix to mark cells that are already part of the path.

From every cell, we attempt to match the word starting at index 0.
If at any point the character doesn't match, goes out of bounds, or the cell is already visited, we stop that path.
If all characters are matched successfully, the word exists in the grid and we return true.

Algorithm

  1. Create a visited matrix of the same size as the board.
  2. For every cell (r, c) in the grid, start a DFS to match the word from index 0.
  3. In DFS (r, c, i):
    • If i == len(word), all characters are matched → return true.
    • If out of bounds, character mismatch, or already visited → return false.
    • Mark the current cell as visited.
    • Recurse to the 4 neighboring cells with i + 1.
    • Unmark the cell (backtrack).
  4. If any DFS call returns true, return true.
  5. If all starts fail, return false.
class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        ROWS, COLS = len(board), len(board[0])
        visited = [[False for _ in range(COLS)] for _ in range(ROWS)]

        def dfs(r, c, i):
            if i == len(word):
                return True
            if (r < 0 or c < 0 or r >= ROWS or c >= COLS or
                word[i] != board[r][c] or visited[r][c]):
                return False

            visited[r][c] = True
            res = (dfs(r + 1, c, i + 1) or
                   dfs(r - 1, c, i + 1) or
                   dfs(r, c + 1, i + 1) or
                   dfs(r, c - 1, i + 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

Time & Space Complexity

  • Time complexity: O(m4n)O(m * 4 ^ n)
  • Space complexity: O(n)O(n)

Where mm is the number of cells in the boardboard and nn is the length of the wordword.


3. Backtracking (Optimal)

Intuition

We want to check if the word can be formed by moving up/down/left/right in the grid, using each cell at most once in a single path.

Instead of keeping a separate visited matrix (extra space), we temporarily mark the current cell as used by replacing its character with a special value (like '#').
This means:

  • If we ever see '#', we know this cell is already in our current path → we can't reuse it.
  • After exploring from that cell, we restore the original character (this is the "backtrack" step), so other paths can use it.

So the idea is:

  • Try every cell as a starting point.
  • Do DFS to match the word character by character.
  • Mark → explore neighbors → unmark.

Algorithm

  1. Let ROWS, COLS be grid size.
  2. Define dfs(r, c, i) meaning: "Can we match word[i...] starting from cell (r, c)?"
  3. Base case: if i == len(word), we matched all characters → return true.
  4. Fail cases: if out of bounds, current cell doesn't match word[i], or cell is already used ('#') → return false.
  5. Mark the cell as used (set it to '#').
  6. Try DFS in 4 directions with i + 1.
  7. Restore the cell back to its original character (backtrack).
  8. Run dfs(r, c, 0) from every cell (r, c). If any returns true, answer is true; otherwise false.
class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        ROWS, COLS = len(board), len(board[0])

        def dfs(r, c, i):
            if i == len(word):
                return True
            if (r < 0 or c < 0 or r >= ROWS or c >= COLS or
                word[i] != board[r][c] or board[r][c] == '#'):
                return False

            board[r][c] = '#'
            res = (dfs(r + 1, c, i + 1) or
                   dfs(r - 1, c, i + 1) or
                   dfs(r, c + 1, i + 1) or
                   dfs(r, c - 1, i + 1))
            board[r][c] = word[i]
            return res

        for r in range(ROWS):
            for c in range(COLS):
                if dfs(r, c, 0):
                    return True
        return False

Time & Space Complexity

  • Time complexity: O(m4n)O(m * 4 ^ n)
  • Space complexity: O(n)O(n)

Where mm is the number of cells in the boardboard and nn is the length of the wordword.


Common Pitfalls

Not Restoring the Cell After Backtracking

When marking a cell as visited by changing its value (e.g., to '#'), forgetting to restore the original character after exploring all neighbors will permanently modify the board. This causes other paths starting from different cells to incorrectly treat valid cells as already visited.

Checking Visited Before Matching Character

Placing the visited check before the character match check can cause subtle bugs. If a cell is marked visited with a special character like '#', comparing board[r][c] != word[i] will correctly fail. However, if using a separate visited structure, the order of checks matters for correctness and clarity.

Forgetting to Match the First Character Before DFS

Starting DFS from every cell without first checking if board[r][c] == word[0] wastes time exploring paths that cannot possibly match the word. While not incorrect, this optimization significantly improves performance on large boards.