Grid Backtracking
Matrix/Grid Patterns (2D)
To solve a maze, you must be willing to walk down a path, realize it's a dead end, and have the wisdom to turn back.
You've seen how backtracking can explore all permutations or combinations. Now, we'll apply that same "try, fail, and undo" logic to navigate the implicit graph of a grid, searching for a path that satisfies a specific set of constraints.
Problem: Word Search (LeetCode Link): Given an m x n
grid of characters and a word
, find if the word exists in the grid. The word can be constructed from letters of sequentially adjacent cells, but the same letter cell may not be used more than once in the word. 💡 HintThis isn't just a traversal. You need to find a specific path. What happens if a path starts correctly but leads to a dead end? 💡 HintHow do you keep track of the path you've taken to avoid reusing cells?
Adapting Backtracking to a Grid
Backtracking is a methodical way of trying out different sequences of decisions until you find one that "works." For grid problems, this means exploring implicit paths through the grid, where each cell can be thought of as a node in a graph.
The main challenge is managing the state—the "path" you're currently on—to avoid reusing cells.
The "Mark and Unmark" Technique
The most common way to manage the path is to modify the grid in-place.
- Mark (Choose): When you move to a cell
(r, c)
, change its value to a special character (like'#'
) to signify, "This cell is currently in my path." - Explore: Make your recursive calls to the neighbors.
- Unmark (Unchoose): After the recursive calls return, you must restore the cell's original character.
board[r][c] = original_char
. This "un-marks" the cell, effectively removing it from the current path and making it available for other branches of the recursion.
Walkthrough: Solving "Word Search"
Let's apply the "Choose, Explore, Unchoose" pattern.
- Main Loop: Iterate through each cell
(r, c)
. Ifboard[r][c]
matches the first letter of theword
, launch a backtracking search from there. - Backtracking Function
backtrack(r, c, index)
:- Success Case: If
index
reaches the end of the word, we've found a valid path. Returntrue
. - Failure Cases: Return
false
if the cell is out of bounds or its character doesn't matchword[index]
. - Choose & Mark:
temp = board[r][c]
, thenboard[r][c] = '#'
. - Explore: Recursively call
backtrack
for all four neighbors withindex + 1
. If any call returnstrue
, we've found a solution, so returntrue
immediately. - Unchoose & Unmark: If no neighbor found a solution, restore the cell:
board[r][c] = temp
. Returnfalse
.
- Success Case: If
def exist(board: list[list[str]], word: str) -> bool:
rows, cols = len(board), len(board[0])
def backtrack(r, c, index):
if index == len(word):
return True
if r < 0 or r >= rows or c < 0 or c >= cols or board[r][c] != word[index]:
return False
# Choose & Mark
temp = board[r][c]
board[r][c] = '#'
# Explore neighbors
found = (backtrack(r + 1, c, index + 1) or
backtrack(r - 1, c, index + 1) or
backtrack(r, c + 1, index + 1) or
backtrack(r, c - 1, index + 1))
# Unchoose & Unmark
board[r][c] = temp
return found
for r in range(rows):
for c in range(cols):
if backtrack(r, c, 0):
return True
return False
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 (backtrack(board, word, r, c, 0)) {
return true;
}
}
}
return false;
}
private boolean backtrack(char[][] board, String word, int r, int c, int index) {
if (index == word.length()) return true;
if (r < 0 || r >= board.length || c < 0 || c >= board[0].length || board[r][c] != word.charAt(index)) {
return false;
}
// Choose & Mark
char temp = board[r][c];
board[r][c] = '#';
// Explore
boolean found = backtrack(board, word, r + 1, c, index + 1) ||
backtrack(board, word, r - 1, c, index + 1) ||
backtrack(board, word, r, c + 1, index + 1) ||
backtrack(board, word, r, c - 1, index + 1);
// Unchoose & Unmark
board[r][c] = temp;
return found;
}
bool backtrack(vector<vector<char>>& board, const string& word, int r, int c, int index) {
if (index == word.length()) return true;
if (r < 0 || r >= board.size() || c < 0 || c >= board[0].size() || board[r][c] != word[index]) {
return false;
}
// Choose & Mark
char temp = board[r][c];
board[r][c] = '#';
// Explore
bool found = backtrack(board, word, r + 1, c, index + 1) ||
backtrack(board, word, r - 1, c, index + 1) ||
backtrack(board, word, r, c + 1, index + 1) ||
backtrack(board, word, r, c - 1, index + 1);
// Unchoose & Unmark
board[r][c] = temp;
return found;
}
bool exist(vector<vector<char>>& board, string word) {
for (int r = 0; r < board.size(); ++r) {
for (int c = 0; c < board[0].size(); ++c) {
if (backtrack(board, word, r, c, 0)) {
return true;
}
}
}
return false;
}
Complexity Analysis
- Time Complexity: , where N is the number of cells in the grid and L is the length of the word. For each cell, we initiate a search. The search can branch out to 3 neighbors (we don't go back to the cell we came from).
- Space Complexity: , where L is the length of the word. This is the maximum depth of the recursion stack.
More Problems & Variations
- N-Queens II (LeetCode Link): How many distinct solutions are there for the N-Queens puzzle? 💡 HintThe core logic is the same as the classic N-Queens, but instead of storing the board, you just increment a counter every time you find a valid placement in the last row.
- Path with Maximum Gold (LeetCode Link): Find the path that collects the most gold from a grid. 💡 HintThis is a backtracking problem where you want to find the best score, not just the first valid path. Your recursive function should return the amount of gold collected from its path. The main loop will call this for every cell and keep track of the maximum returned value.
Bonus Points: From Grids to Game AI
This exact "Choose, Explore, Unchoose" pattern is fundamental in artificial intelligence, especially for game-playing bots. An AI for a game like chess or checkers explores future moves by:
- Choosing a move to make.
- Exploring the possible counter-moves from the opponent.
- Unchoosing the move to explore other possibilities from the original board state. Behind every smart decision is a thousand paths the AI had the discipline to back out of.