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.
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 gridk
represents the index of the character we're currently trying to match in the word
Base Cases:
-
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]
-
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:
- Calculates new coordinates
(x, y)
- Validates boundaries and checks if the cell isn't already visited (not marked as "0")
- Recursively explores from that position for the next character
k + 1
- 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 EvaluatorExample 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
Which of the following problems can be solved with backtracking (select multiple)
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!