Facebook Pixel

79. Word Search

Problem Description

You are given an m x n grid filled with characters and a string word. Your task is to determine if the word can be found in the grid.

The word can be formed by connecting letters from adjacent cells in the grid, where "adjacent" means cells that share an edge (horizontally or vertically neighboring - not diagonally). You can start from any cell in the grid.

Key constraints:

  • Each cell can only be used once in forming the word
  • You must move to adjacent cells (up, down, left, or right) to form consecutive letters of the word
  • The same cell cannot be revisited within a single word path

For example, if you have a 3x4 grid:

[['A','B','C','E'],
 ['S','F','C','S'],
 ['A','D','E','E']]

And the word is "ABCCED", you can find it by starting at position (0,0) with 'A', then moving right to 'B', right to 'C', down to 'C', down to 'E', and left to 'D'. This would return true.

However, if the word is "ABCB", it would return false because after using the 'B' at position (0,1), you cannot reuse it to form the second 'B' in the word.

The function should return true if the word exists in the grid following these rules, and false otherwise.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: The grid can be viewed as a graph where each cell is a node, and edges connect to adjacent cells (up, down, left, right). We need to find a path through this graph that spells out the word.

Is it a tree?

  • No: The grid structure is not a tree - it can have cycles and multiple paths between nodes. A cell can be reached from multiple directions.

Is the problem related to directed acyclic graphs?

  • No: The grid doesn't have a directed acyclic structure. We can move in any direction (up, down, left, right) and potentially create cycles if we weren't tracking visited cells.

Is the problem related to shortest paths?

  • No: We're not looking for the shortest path. We need to find if ANY valid path exists that forms the word.

Does the problem involve connectivity?

  • No: We're not checking if components are connected or finding connected components. We're searching for a specific sequence of characters.

Does the problem have small constraints?

  • Yes: The grid dimensions are typically small (m × n), and we need to explore paths systematically. The constraint that each cell can only be used once in a path makes this a constrained search problem.

DFS/backtracking?

  • Yes: This is the perfect fit. We need to explore paths depth-first, and when a path doesn't work out, we need to backtrack (marking cells as unvisited again) and try alternative paths.

Conclusion: The flowchart suggests using DFS with backtracking for the Word Search problem. This matches the solution approach where we recursively explore each possible path from every starting position, marking cells as visited during exploration and unmarking them when backtracking.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Think of this problem like trying to find a specific route on a map where you can only travel to adjacent locations and can't revisit any place you've already been. You're essentially playing a maze game where you need to spell out a word by walking through the grid.

The key insight is that we need to explore all possible paths systematically. When we stand on a cell, we have up to 4 choices for the next move (up, down, left, right). This branching nature of choices naturally leads us to think about recursion - at each step, we try one direction, and if it works, we continue from there; if it doesn't work, we come back and try another direction.

The critical challenge is ensuring we don't use the same cell twice in our path. Imagine leaving breadcrumbs as you walk through the maze - you need to mark where you've been. But here's the clever part: if a path doesn't lead to the complete word, we need to pick up our breadcrumbs so that cell can be used in a different path attempt. This is the essence of backtracking.

Why start from every cell? Because we don't know where the word begins in the grid. Any cell could be the starting point of our word. So we systematically try starting from each cell - if we find the word starting from any position, we're done.

The DFS approach emerges naturally because we want to follow one path completely before trying another. It's like following one branch of possibilities all the way to the end before backing up and trying a different branch. We go as deep as possible (trying to match as many characters as we can), and only when we hit a dead end (wrong character or out of bounds) do we backtrack and try a different route.

The temporary marking strategy (board[i][j] = "0") is our way of saying "I'm currently using this cell" without needing extra space for a visited array. Once we're done exploring from that cell, we restore its original value, allowing it to be used in other path attempts from different starting points.

Learn more about Depth-First Search and Backtracking patterns.

Solution Approach

The solution implements a DFS with backtracking strategy. Let's walk through the implementation step by step:

Main Function Structure: The main function exist() serves as the entry point that tries every cell (i, j) in the grid as a potential starting position. It uses a generator expression with any() to efficiently stop as soon as a valid path is found:

return any(dfs(i, j, 0) for i in range(m) for j in range(n))

DFS Helper Function: The core logic lies in the dfs(i, j, k) function, where:

  • i, j represents the current position in the grid
  • k represents the index of the character we're currently trying to match in the word

Base Cases:

  1. Success Case: If k == len(word) - 1, we've reached the last character. We simply check if the current cell matches this last character:

    if k == len(word) - 1:
        return board[i][j] == word[k]
  2. Failure Case: If the current cell doesn't match the expected character, we immediately return False:

    if board[i][j] != word[k]:
        return False

Backtracking Mechanism: The key to preventing cell reuse is the marking strategy:

c = board[i][j]        # Store original character
board[i][j] = "0"      # Mark as visited
# ... explore neighbors ...
board[i][j] = c        # Restore original character (backtrack)

This in-place modification eliminates the need for a separate visited matrix, saving O(m×n) space.

Exploring Neighbors: The code uses an elegant approach with pairwise() to generate direction pairs:

for a, b in pairwise((-1, 0, 1, 0, -1)):
    x, y = i + a, j + b

This creates pairs: (-1, 0), (0, 1), (1, 0), (0, -1) representing up, right, down, and left movements.

For each neighbor, the code:

  1. Calculates new coordinates (x, y)
  2. Validates boundaries and checks if the cell isn't already visited (not marked as "0")
  3. Recursively explores from that position for the next character k + 1
  4. Returns True immediately if any direction succeeds (short-circuit evaluation)

Time Complexity: O(m × n × 4^L) where L is the length of the word. We try each cell as a starting point O(m×n), and from each position, we can branch in up to 4 directions for each character.

Space Complexity: O(L) for the recursion stack depth, which can go as deep as the word length. The in-place marking strategy avoids additional space for tracking visited cells.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's trace through finding the word "SEE" in this small grid:

[['A','B'],
 ['S','E']]

Step 1: Try starting from (0,0) - 'A'

  • Check if 'A' == 'S' (first letter of "SEE")? No, return False
  • Move to next starting position

Step 2: Try starting from (0,1) - 'B'

  • Check if 'B' == 'S'? No, return False
  • Move to next starting position

Step 3: Try starting from (1,0) - 'S'

  • Check if 'S' == 'S'? Yes! Continue with DFS
  • Mark current cell: board[1][0] = '0' (temporarily)
  • Now looking for 'E' (index 1 of "SEE")
  • Try neighbors:
    • Up (0,0): 'A' != 'E', fail
    • Right (1,1): 'E' == 'E'! Match found
      • Mark board[1][1] = '0'
      • Now looking for 'E' (index 2 of "SEE")
      • Try neighbors:
        • Up (0,1): 'B' != 'E', fail
        • Right: out of bounds
        • Down: out of bounds
        • Left (1,0): '0' (visited), skip
      • No valid neighbors, backtrack
      • Restore board[1][1] = 'E'
    • Down: out of bounds
    • Left: out of bounds
  • All directions failed, backtrack
  • Restore board[1][0] = 'S'

Step 4: Try starting from (1,1) - 'E'

  • Check if 'E' == 'S'? No, return False

Result: "SEE" cannot be found, return False

Now let's find "SE" in the same grid:

Starting from (1,0) - 'S'

  • 'S' == 'S'? Yes!
  • Mark board[1][0] = '0'
  • Looking for 'E' (index 1, which is last character)
  • Try Right (1,1): 'E' == 'E'? Yes!
  • Since we've matched the last character, return True

The path S→E successfully spells "SE", so the function returns True.

Solution Implementation

1class Solution:
2    def exist(self, board: List[List[str]], word: str) -> bool:
3        """
4        Determines if a word exists in the board by traversing adjacent cells.
5      
6        Args:
7            board: 2D grid of characters
8            word: Target word to search for
9          
10        Returns:
11            True if word exists in board, False otherwise
12        """
13      
14        def backtrack(row: int, col: int, word_index: int) -> bool:
15            """
16            Performs DFS with backtracking to search for the word.
17          
18            Args:
19                row: Current row position in board
20                col: Current column position in board
21                word_index: Current index in the word being matched
22              
23            Returns:
24                True if word can be formed from current position
25            """
26            # Base case: reached the last character of the word
27            if word_index == len(word) - 1:
28                return board[row][col] == word[word_index]
29          
30            # Current cell doesn't match the expected character
31            if board[row][col] != word[word_index]:
32                return False
33          
34            # Mark current cell as visited by temporarily changing its value
35            original_char = board[row][col]
36            board[row][col] = "#"  # Use "#" as visited marker
37          
38            # Explore all four directions: up, right, down, left
39            directions = [(-1, 0), (0, 1), (1, 0), (0, -1)]
40          
41            for delta_row, delta_col in directions:
42                next_row = row + delta_row
43                next_col = col + delta_col
44              
45                # Check if next position is valid and not visited
46                if (0 <= next_row < rows and 
47                    0 <= next_col < cols and 
48                    board[next_row][next_col] != "#"):
49                  
50                    # Recursively search from the next position
51                    if backtrack(next_row, next_col, word_index + 1):
52                        board[row][col] = original_char  # Restore before returning
53                        return True
54          
55            # Backtrack: restore the original character
56            board[row][col] = original_char
57            return False
58      
59        # Get board dimensions
60        rows = len(board)
61        cols = len(board[0])
62      
63        # Try starting the search from every cell in the board
64        for start_row in range(rows):
65            for start_col in range(cols):
66                if backtrack(start_row, start_col, 0):
67                    return True
68      
69        return False
70
1class Solution {
2    // Grid dimensions
3    private int rows;
4    private int cols;
5    // Target word to search
6    private String targetWord;
7    // Reference to the board
8    private char[][] grid;
9
10    /**
11     * Determines if the word exists in the board by searching all possible paths
12     * @param board 2D character grid
13     * @param word target word to find
14     * @return true if word exists in the board, false otherwise
15     */
16    public boolean exist(char[][] board, String word) {
17        // Initialize instance variables
18        rows = board.length;
19        cols = board[0].length;
20        this.targetWord = word;
21        this.grid = board;
22      
23        // Try starting the search from every cell in the grid
24        for (int row = 0; row < rows; row++) {
25            for (int col = 0; col < cols; col++) {
26                // If we find the word starting from current position, return true
27                if (dfs(row, col, 0)) {
28                    return true;
29                }
30            }
31        }
32      
33        // Word not found in any path
34        return false;
35    }
36
37    /**
38     * Depth-first search to find the word starting from position (row, col)
39     * @param row current row position
40     * @param col current column position
41     * @param charIndex current character index in the target word
42     * @return true if remaining word is found from this position
43     */
44    private boolean dfs(int row, int col, int charIndex) {
45        // Base case: reached the last character of the word
46        if (charIndex == targetWord.length() - 1) {
47            return grid[row][col] == targetWord.charAt(charIndex);
48        }
49      
50        // Current cell doesn't match the required character
51        if (grid[row][col] != targetWord.charAt(charIndex)) {
52            return false;
53        }
54      
55        // Mark current cell as visited by temporarily changing its value
56        char originalChar = grid[row][col];
57        grid[row][col] = '0';
58      
59        // Direction vectors for exploring 4 adjacent cells (up, right, down, left)
60        int[] directions = {-1, 0, 1, 0, -1};
61      
62        // Explore all 4 directions
63        for (int dir = 0; dir < 4; dir++) {
64            int nextRow = row + directions[dir];
65            int nextCol = col + directions[dir + 1];
66          
67            // Check if next position is valid and unvisited, then continue search
68            if (nextRow >= 0 && nextRow < rows && 
69                nextCol >= 0 && nextCol < cols && 
70                grid[nextRow][nextCol] != '0' && 
71                dfs(nextRow, nextCol, charIndex + 1)) {
72                return true;
73            }
74        }
75      
76        // Backtrack: restore the original character
77        grid[row][col] = originalChar;
78      
79        // No valid path found from this position
80        return false;
81    }
82}
83
1class Solution {
2public:
3    bool exist(vector<vector<char>>& board, string word) {
4        int rows = board.size();
5        int cols = board[0].size();
6      
7        // Direction vectors for moving up, right, down, left
8        // dirs[i] and dirs[i+1] form a coordinate pair for each direction
9        int dirs[5] = {-1, 0, 1, 0, -1};
10      
11        // DFS function to search for the word starting from position (row, col)
12        // index represents the current character index in the word we're matching
13        function<bool(int, int, int)> dfs = [&](int row, int col, int index) -> bool {
14            // Base case: reached the last character of the word
15            if (index == word.size() - 1) {
16                return board[row][col] == word[index];
17            }
18          
19            // Current cell doesn't match the character we're looking for
20            if (board[row][col] != word[index]) {
21                return false;
22            }
23          
24            // Temporarily mark the current cell as visited by storing its value
25            char originalChar = board[row][col];
26            board[row][col] = '0';  // Use '0' as a visited marker
27          
28            // Explore all four directions
29            for (int dir = 0; dir < 4; ++dir) {
30                int nextRow = row + dirs[dir];
31                int nextCol = col + dirs[dir + 1];
32              
33                // Check if the next position is valid and unvisited
34                if (nextRow >= 0 && nextRow < rows && 
35                    nextCol >= 0 && nextCol < cols && 
36                    board[nextRow][nextCol] != '0') {
37                  
38                    // Recursively search from the next position
39                    if (dfs(nextRow, nextCol, index + 1)) {
40                        return true;
41                    }
42                }
43            }
44          
45            // Backtrack: restore the original character
46            board[row][col] = originalChar;
47            return false;
48        };
49      
50        // Try starting the search from every cell in the board
51        for (int i = 0; i < rows; ++i) {
52            for (int j = 0; j < cols; ++j) {
53                if (dfs(i, j, 0)) {
54                    return true;  // Word found
55                }
56            }
57        }
58      
59        return false;  // Word not found
60    }
61};
62
1/**
2 * Determines if a word exists in a 2D board by searching adjacent cells
3 * @param board - 2D array of characters representing the board
4 * @param word - Target word to search for in the board
5 * @returns true if the word exists in the board, false otherwise
6 */
7function exist(board: string[][], word: string): boolean {
8    const rows: number = board.length;
9    const cols: number = board[0].length;
10  
11    // Direction vectors for exploring 4 adjacent cells (up, right, down, left)
12    // Used as pairs: (directions[i], directions[i+1]) for row and column offsets
13    const directions: number[] = [-1, 0, 1, 0, -1];
14  
15    /**
16     * Depth-first search to find the word starting from position (row, col)
17     * @param row - Current row position in the board
18     * @param col - Current column position in the board
19     * @param wordIndex - Current index in the word being matched
20     * @returns true if the remaining word can be found from this position
21     */
22    const depthFirstSearch = (row: number, col: number, wordIndex: number): boolean => {
23        // Base case: reached the last character of the word
24        if (wordIndex === word.length - 1) {
25            return board[row][col] === word[wordIndex];
26        }
27      
28        // Current cell doesn't match the expected character
29        if (board[row][col] !== word[wordIndex]) {
30            return false;
31        }
32      
33        // Temporarily mark the current cell as visited by storing its value
34        const originalChar: string = board[row][col];
35        board[row][col] = '0'; // Mark as visited
36      
37        // Explore all 4 adjacent directions
38        for (let direction = 0; direction < 4; direction++) {
39            const nextRow: number = row + directions[direction];
40            const nextCol: number = col + directions[direction + 1];
41          
42            // Check if the next position is within bounds
43            const isValidPosition: boolean = 
44                nextRow >= 0 && nextRow < rows && 
45                nextCol >= 0 && nextCol < cols;
46          
47            // If valid position, not visited, and continues to match the word
48            if (isValidPosition && 
49                board[nextRow][nextCol] !== '0' && 
50                depthFirstSearch(nextRow, nextCol, wordIndex + 1)) {
51                return true;
52            }
53        }
54      
55        // Backtrack: restore the original character
56        board[row][col] = originalChar;
57        return false;
58    };
59  
60    // Try starting the search from every cell in the board
61    for (let row = 0; row < rows; row++) {
62        for (let col = 0; col < cols; col++) {
63            if (depthFirstSearch(row, col, 0)) {
64                return true;
65            }
66        }
67    }
68  
69    return false;
70}
71

Time and Space Complexity

Time Complexity: O(m × n × 3^k)

The algorithm iterates through each cell in the m × n grid as a potential starting point, giving us O(m × n) starting positions. For each starting position, we perform a depth-first search (DFS) to find the word.

During the DFS traversal:

  • At the first character, we can explore up to 4 directions
  • For subsequent characters (positions 2 through k), we can explore at most 3 directions since we mark the current cell as visited (by setting it to "0") and cannot return to it
  • The search tree has a maximum depth of k (the word length)
  • The branching factor is at most 3 for levels 2 through k

Therefore, the worst-case number of recursive calls from each starting position is O(4 × 3^(k-1)), which simplifies to O(3^k).

Combining with all possible starting positions: O(m × n × 3^k)

Space Complexity: O(min(m × n, k))

The space complexity is determined by the recursion call stack depth. The maximum depth of recursion is bounded by:

  • The length of the word k (we stop when we've matched all characters)
  • The total number of cells m × n (we can't visit more cells than exist in the grid)

Since the recursion depth cannot exceed either of these limits, the space complexity is O(min(m × n, k)).

Note that the algorithm modifies the board in-place for marking visited cells, so no additional space is needed for tracking visited cells.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Forgetting to Restore the Board State (Backtracking)

One of the most critical pitfalls is failing to properly restore the original character after exploring a path. This permanently corrupts the board and prevents other valid paths from being found.

Incorrect Implementation:

def backtrack(row, col, word_index):
    if word_index == len(word) - 1:
        return board[row][col] == word[word_index]
  
    if board[row][col] != word[word_index]:
        return False
  
    board[row][col] = "#"  # Mark as visited
  
    for delta_row, delta_col in directions:
        next_row = row + delta_row
        next_col = col + delta_col
      
        if (0 <= next_row < rows and 
            0 <= next_col < cols and 
            board[next_row][next_col] != "#"):
          
            if backtrack(next_row, next_col, word_index + 1):
                return True  # ❌ WRONG: Forgot to restore!
  
    # ❌ Missing restoration here too
    return False

Correct Implementation:

def backtrack(row, col, word_index):
    # ... base cases ...
  
    original_char = board[row][col]
    board[row][col] = "#"
  
    for delta_row, delta_col in directions:
        # ... validation ...
        if backtrack(next_row, next_col, word_index + 1):
            board[row][col] = original_char  # ✅ Restore before returning True
            return True
  
    board[row][col] = original_char  # ✅ Always restore before returning False
    return False

2. Using the Visited Marker as a Valid Character

If the visited marker character (like "#" or "0") could potentially appear in the input board, it would cause false negatives.

Problem Example:

board = [['A', '#', 'B']]
word = "A#B"
# If we use '#' as visited marker, we can't distinguish between 
# a visited cell and a cell that actually contains '#'

Solution: Either ensure the marker is outside the valid character set, or use a separate visited set:

def backtrack(row, col, word_index, visited):
    if (row, col) in visited:
        return False
  
    # ... validation ...
  
    visited.add((row, col))
    # ... explore neighbors ...
    visited.remove((row, col))  # Backtrack

3. Early Return Without Restoration

When finding a successful path, forgetting to restore the board before returning can affect subsequent searches if the function is called multiple times.

Problematic Pattern:

for delta_row, delta_col in directions:
    if valid_position and backtrack(next_row, next_col, word_index + 1):
        return True  # ❌ Might forget to restore here

Better Pattern:

found = False
for delta_row, delta_col in directions:
    if valid_position and backtrack(next_row, next_col, word_index + 1):
        found = True
        break

board[row][col] = original_char  # ✅ Always restore
return found

4. Incorrect Boundary Checking Order

Checking array bounds after accessing the array element causes IndexError:

Wrong:

if board[next_row][next_col] != "#" and 0 <= next_row < rows and 0 <= next_col < cols:
    # ❌ Accesses board before checking bounds

Correct:

if 0 <= next_row < rows and 0 <= next_col < cols and board[next_row][next_col] != "#":
    # ✅ Checks bounds first (short-circuit evaluation)

5. Not Handling Edge Cases

Failing to handle edge cases like empty board or empty word:

Robust Implementation:

def exist(self, board: List[List[str]], word: str) -> bool:
    if not board or not board[0] or not word:
        return False if word else True  # Empty word is found in any board
  
    # ... rest of the implementation
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following problems can be solved with backtracking (select multiple)


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More