Chapter 14 Dynamic Programming and Backtracking
This chapter covers Dynamic Programming and Backtracking — two fundamental algorithmic strategies that appear repeatedly across coding interview problems. Dynamic Programming solves problems by breaking them into overlapping subproblems and caching results to avoid redundant computation. Backtracking explores all possible solutions by incrementally building candidates and pruning dead ends early.
14.0.1 Dynamic Programming
Dynamic Programming (DP) is a method to simplify complicated problems by breaking them down into simpler sub-problems in a recursive manner. The key insight is that sub-problems typically repeat and overlap, allowing us to store and reuse solutions to avoid redundant computation.
There are two main approaches to implementing DP solutions:
14.0.1.1 Memoization (Top-Down)
Memoization is a top-down recursive approach with caching. It solves the problem by recursively breaking it down into smaller subproblems and storing (caching) the results to avoid redundant calculations.
Key Characteristics:
- Starts from the original problem and recursively breaks it down
- Uses a cache (typically a HashMap or array) to store computed results
- Only computes subproblems that are actually needed
- Natural recursive structure that mirrors the problem definition
Template:
Map<Key, Result> memo = new HashMap<>();
public Result solve(Problem problem) {
// Base case
if (isBaseCase(problem)) {
return baseResult;
}
// Check cache
if (memo.containsKey(problem)) {
return memo.get(problem);
}
// Compute result recursively
Result result = combineSubproblems(
solve(subproblem1),
solve(subproblem2)
);
// Store in cache
memo.put(problem, result);
return result;
}Example: Fibonacci with Memoization
Map<Integer, Long> memo = new HashMap<>();
public long fibonacci(int n) {
if (n <= 1) return n;
if (memo.containsKey(n)) {
return memo.get(n);
}
long result = fibonacci(n - 1) + fibonacci(n - 2);
memo.put(n, result);
return result;
}Pros:
- Intuitive - mirrors the recursive problem definition
- Only computes necessary subproblems (lazy evaluation)
- Easy to convert from naive recursion (just add caching)
Cons:
- Recursion overhead (stack space and function calls)
- Risk of stack overflow for deep recursion
- Slightly slower due to cache lookups and recursion
Time Complexity: Same as tabulation (typically \(O(n \times m)\) for 2D DP) Space Complexity: \(O(n)\) for cache + \(O(n)\) for recursion stack = \(O(n)\) total
14.0.1.2 Tabulation (Bottom-Up)
Tabulation is a bottom-up iterative approach that builds a table (array) by solving smaller subproblems first and using their results to solve larger ones.
Key Characteristics:
- Starts from base cases and builds up to the final solution
- Uses an array/table to store results iteratively
- Computes all subproblems (even if not needed for final answer)
- No recursion - purely iterative
Template:
public Result solve(Problem problem) {
// Initialize DP table
Result[] dp = new Result[size];
// Base case(s)
dp[0] = baseResult;
// Fill table iteratively
for (int i = 1; i < size; i++) {
dp[i] = combineSubproblems(dp[i-1], dp[i-2], ...);
}
return dp[size - 1];
}Example: Fibonacci with Tabulation
public long fibonacci(int n) {
if (n <= 1) return n;
long[] dp = new long[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}Pros:
- No recursion overhead (no stack space, faster)
- Easier to optimize space (often can reduce from O(n) to O(1))
- More efficient for problems where most subproblems are needed
- No risk of stack overflow
Cons:
- Less intuitive than memoization for some problems
- Computes all subproblems (even unnecessary ones)
- Requires determining the correct iteration order
Time Complexity: Same as memoization (typically \(O(n \times m)\) for 2D DP) Space Complexity: \(O(n)\) for table (often optimizable to \(O(1)\))
14.0.1.2.1 Unbounded Knapsack Pattern
The Unbounded Knapsack pattern is a tabulation-based dynamic programming technique for problems where you can use items (coins, elements, etc.) an unlimited number of times to achieve a target. Unlike the 0/1 Knapsack where each item is used at most once, unbounded knapsack allows infinite repetitions.
Core Concept:
Given a set of items with certain properties (value, weight, denomination) and a target, determine: - The number of ways to reach the target - The minimum/maximum value achievable - Whether the target is reachable
Key characteristic: Items can be reused multiple times.
Combinations vs Permutations:
A critical distinction in unbounded knapsack problems is whether order matters:
- Combinations:
{1, 2}is the same as{2, 1}→ Use coins in outer loop - Permutations:
{1, 2}differs from{2, 1}→ Use amount in outer loop
For Combinations (most common):
for (int coin : coins) { // Outer: iterate items
for (int i = coin; i <= amount; i++) { // Inner: iterate target
dp[i] += dp[i - coin];
}
}For Permutations:
for (int i = 1; i <= amount; i++) { // Outer: iterate target
for (int coin : coins) { // Inner: iterate items
if (i >= coin) {
dp[i] += dp[i - coin];
}
}
}Standard Recurrence Relation:
For counting combinations to make amount i:
\[ \text{dp}[i] = \sum_{\text{coin} \in \text{coins}} \text{dp}[i - \text{coin}] \]
Where dp[i] represents the number of ways to make amount i.
Base case: dp[0] = 1 (one way to make 0: use no items)
Why Loop Order Matters:
Visual Example: coins = [1, 2], amount = 3
Combinations approach (coins outer):
- Process coin 1: builds {1}, {1,1}, {1,1,1}
- Process coin 2: builds {2}, {1,2}, {2,1} → but {1,2} already counted as {1,1,1}
- Result: Correctly counts {1,1,1}, {1,2} (4 combinations)
Permutations approach (amount outer):
- Process amount 1: {1}
- Process amount 2: {1,1}, {2}
- Process amount 3: {1,1,1}, {1,2}, {2,1} (6 permutations)
Time and Space Complexity:
- Time: \(O(n \times m)\) where \(n\) is the target amount and \(m\) is the number of items
- Space: \(O(n)\) using 1D DP array (can be optimized from 2D)
Standard Implementation Pattern:
public int unboundedKnapsack(int target, int[] items) {
int[] dp = new int[target + 1];
dp[0] = 1; // Base case
// Outer loop: items (for combinations)
for (int item : items) {
// Inner loop: all reachable targets
for (int i = item; i <= target; i++) {
dp[i] += dp[i - item];
}
}
return dp[target];
}Variants:
- Count combinations: How many ways to make the target?
- Coin Change II - Classic unbounded knapsack counting
- Minimum items needed: What’s the fewest items to reach target?
- Coin Change - Find minimum number of coins
- Recurrence:
dp[i] = min(dp[i], dp[i - item] + 1)
- Maximum value: What’s the maximum value achievable?
- Recurrence:
dp[i] = max(dp[i], dp[i - weight] + value)
- Recurrence:
- Reachability: Can the target be reached?
- Use boolean DP:
dp[i] = dp[i] || dp[i - item]
- Use boolean DP:
14.0.1.3 Memoization vs Tabulation Comparison
| Aspect | Memoization | Tabulation |
|---|---|---|
| Direction | Top-down (recursive) | Bottom-up (iterative) |
| Implementation | Recursion + cache | Iteration + table |
| Subproblems | Only needed ones | All subproblems |
| Intuition | Natural recursive structure | Build from base cases |
| Stack Space | \(O(n)\) recursion stack | \(O(1)\) no recursion |
| Speed | Slightly slower | Slightly faster |
| Space Optimization | Harder | Easier (often → \(O(1)\)) |
| Risk | Stack overflow | None |
When to use Memoization:
- Problem naturally expressed recursively
- Only a subset of subproblems needed
- Quick conversion from naive recursion
- Tree-based or non-sequential dependencies
When to use Tabulation:
- Most/all subproblems needed for solution
- Want to avoid recursion overhead
- Need space optimization (\(O(1)\) possible)
- Sequential dependencies (Fibonacci, climbing stairs)
14.0.1.4 State Optimization
Beyond choosing memoization or tabulation, you can often reduce space complexity by observing that you only need recent states:
Example: Fibonacci Space Optimization
// From O(n) space
long[] dp = new long[n + 1];
// To O(1) space
long prev2 = 0, prev1 = 1;
for (int i = 2; i <= n; i++) {
long current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}This optimization is more commonly applied to tabulation than memoization.
14.0.1.5 Kadane’s Algorithm
Kadane’s algorithm is a space-optimized tabulation DP technique for finding the maximum sum (or product) of a contiguous subarray. It demonstrates how tabulation DP can be reduced from \(O(n)\) space to \(O(1)\) space, making it one of the most elegant examples of DP optimization.
14.0.1.5.1 Core Concept
The key insight is to maintain a running state representing the best subarray ending at the current position. At each element, we make an optimal decision:
- Start fresh from the current element
- Extend the previous subarray by including the current element
This decision is captured by the recurrence relation:
\[ \text{dp}[i] = \max(\text{nums}[i], \text{dp}[i-1] + \text{nums}[i]) \]
where \(\text{dp}[i]\) represents the maximum sum of a subarray ending at index i.
State Definition:
current: Maximum sum ending at the current positionmaxSoFar: Global maximum across all positions
Why It’s Dynamic Programming:
- Optimal Substructure: The best subarray ending at position \(i\) depends on the best subarray ending at position \(i-1\)
- Overlapping Subproblems: We reuse the previous state (
current) to compute the next state - Memoization: We only need to track one previous state (space-optimized DP)
14.0.1.5.2 Key Insight
Why does max(nums[i], current + nums[i]) work?
nums[i]: Start a new subarray from this element (abandon previous state)current + nums[i]: Extend the previous best subarray (build on previous state)
Decision Logic:
- If the previous sum
currentis negative, starting fresh fromnums[i]gives a better result - If the previous sum is positive, extending it by adding
nums[i]is better
This is the essence of optimal substructure in DP.
14.0.1.5.3 Standard Implementation (Space-Optimized Tabulation)
public int maxSubArray(int[] nums) {
int current = nums[0]; // Best subarray ending here
int maxSoFar = nums[0]; // Global maximum
for (int i = 1; i < nums.length; i++) {
// DP recurrence: max(start fresh, extend previous)
current = Math.max(nums[i], current + nums[i]);
// Track global maximum
maxSoFar = Math.max(maxSoFar, current);
}
return maxSoFar;
}This is \(O(1)\) space-optimized tabulation DP:
- Instead of storing
dp[i]for all \(i\), we only trackcurrent(latest state) - Original 1D DP table:
int[] dp = new int[n] - Optimized: Two variables
currentandmaxSoFar
14.0.1.5.4 Kadane’s Algorithm Variants
1. Maximum Product Subarray
For products instead of sums, we need to track both max and min because negative numbers flip signs. A large negative product can become a large positive when multiplied by another negative.
Modified Recurrence: \[ \begin{align} \text{maxProduct}[i] &= \max(\text{nums}[i], \text{maxProduct}[i-1] \times \text{nums}[i], \text{minProduct}[i-1] \times \text{nums}[i]) \\ \text{minProduct}[i] &= \min(\text{nums}[i], \text{maxProduct}[i-1] \times \text{nums}[i], \text{minProduct}[i-1] \times \text{nums}[i]) \end{align} \]
See Maximum Product Subarray (LeetCode 152) for the complete implementation.
2. Circular Maximum Subarray
For circular arrays, the maximum can wrap around the boundary. Use Kadane’s algorithm twice: - Case 1: Standard Kadane (no wrap) - Case 2: Total sum - minimum subarray (wrap around)
See Maximum Sum Circular Subarray (LeetCode 918).
3. Stock Price Problems
Many stock problems are variants of Kadane’s algorithm with state tracking:
- Best Time to Buy and Sell Stock: Find maximum prices[j] - prices[i] where \(j > i\)
- Transform to maximum subarray of price differences
- Stock with multiple transactions: Add transaction limit state to DP
14.0.1.5.5 Complexity Analysis
- Time: \(O(n)\) - Single pass through the array
- Space: \(O(1)\) - Only uses constant variables (space-optimized DP)
Comparison to Naive Approach:
- Naive: Try all \(O(n^2)\) subarrays → \(O(n^2)\) time
- Kadane’s DP: Reuse previous state → \(O(n)\) time
This demonstrates DP’s power: reducing exponential/polynomial time to linear time by exploiting optimal substructure.
14.0.1.5.6 When to Recognize Kadane’s Algorithm
Use Kadane’s algorithm when the problem asks: - “Find the maximum/minimum sum/product of a contiguous subarray”
- “Best profit from a sequence of transactions” (with state tracking)
- Linear optimization over a contiguous subsequence
Red flags that it’s NOT Kadane’s:
- Need to find all subarrays (enumeration) → Backtracking or brute force
- Non-contiguous elements allowed → Different DP pattern (e.g., Longest Increasing Subsequence)
14.0.2 Backtracking Algorithms
Backtracking is a fundamental algorithmic technique for exploring all possible solutions to a problem by incrementally building candidates and abandoning (“backtracking”) from candidates that cannot lead to a valid solution.
There are three main backtracking patterns, each suited to different problem types:
- Choose-Explore-Unchoose (Mutable State) - Most common for combinations/subsets
- Pass-by-Value (Immutable State) - Cleaner but uses more memory
- Mark-Unmark (Grid/Graph Traversal) - For 2D grid or graph problems
14.0.2.1 Pattern 1: Choose-Explore-Unchoose (Mutable State)
Best for: Combination/subset problems where you build solutions incrementally using shared mutable state.
Core Concept - 2D Traversal Mental Model:
Backtracking is fundamentally a 2-dimensional traversal of the decision space to enumerate all possible combinations:
- Outer Dimension (Recursion Parameter): Controls the depth/position in the decision tree
- Represented by recursion parameters like
index,depth,remaining,start - Each recursive call advances to the next level (next digit, next position, next item)
- Analogous to the outer loop variable in nested loops
- Represented by recursion parameters like
- Inner Dimension (For-Loop): Controls the choice at the current depth
- Represented by the for-loop within
backtrack()that iterates through available choices - Each iteration explores a different branch at the current level
- Analogous to the inner loop variable in nested loops
- Represented by the for-loop within
Mental Model: Backtracking = Nested loops implemented via recursion
Traditional Nested Loops (iterative):
for (int depth = 0; depth < n; depth++) { // Outer dimension
for (int choice = 0; choice < m; choice++) { // Inner dimension
process(depth, choice);
}
}
Backtracking (recursive):
void backtrack(int depth) { // Outer dimension
if (depth == n) return;
for (int choice = 0; choice < m; choice++) { // Inner dimension
choose(choice);
backtrack(depth + 1); // Advance outer dimension
unchoose(choice);
}
}
Key Insight: The for-loop explores the breadth (all choices at current position), while recursion explores the depth (sequential positions). Together, they systematically visit every (depth, choice) coordinate in the decision space.
The Choose-Explore-Unchoose Pattern implements this 2D traversal:
- Choose: Select a choice from the inner dimension (for-loop iteration)
- Explore: Move to the next depth in the outer dimension (recursive call)
- Unchoose: Backtrack to try the next choice in the inner dimension
Visual Example - Letter Combinations “23”:
Depth 0: choices = ['a', 'b', 'c']
Depth 1: choices = ['d', 'e', 'f']
Traversal as 2D grid:
Choice 0 Choice 1 Choice 2
Depth 0: 'a' 'b' 'c'
Depth 1: 'd' 'e' 'f'
Paths visited:
(0,'a') → (1,'d') = "ad"
(0,'a') → (1,'e') = "ae"
(0,'a') → (1,'f') = "af"
(0,'b') → (1,'d') = "bd"
... (9 total paths = 3 × 3)
Important Nuance: The inner dimension size can vary based on the outer dimension:
- In Combination Sum with unbounded reuse, choices narrow as depth increases
- The “grid” is jagged/triangular, not rectangular
- Pruning can skip invalid (depth, choice) coordinates
This pattern enables exhaustive search while maintaining a single shared state object, making it memory-efficient compared to creating new states for each recursive call.
14.0.2.2 Why Unchoose Matters
The unchoose step is critical for correctness. Without it: - The state accumulates all choices from all branches - Alternative branches start with polluted state - You cannot explore independent paths from the same starting point
Example: Finding combinations that sum to a target:
Without unchoose:
Choose 1 → state=[1]
Choose 2 → state=[1,2] ✓ Found!
Choose 3 → state=[1,2,3] ← Wrong! Should be [1,3]
With unchoose:
Choose 1 → state=[1]
Choose 2 → state=[1,2] ✓ Found!
Unchoose → state=[1]
Choose 3 → state=[1,3] ✓ Correct!
Unchoose → state=[1]
Unchoose → state=[]
14.0.2.3 Standard Template
public void backtrack(State current, Parameters params, Result result) {
// Base case: found a valid solution
if (isValidSolution(current)) {
result.add(new State(current)); // Must copy!
return;
}
// Base case: pruning - abandon invalid paths
if (shouldPrune(current, params)) {
return;
}
// Iterate through all possible choices
for (Choice choice : getAvailableChoices(params)) {
// 1. CHOOSE: Make the decision
current.add(choice);
// 2. EXPLORE: Recurse with updated state
backtrack(current, updateParams(choice), result);
// 3. UNCHOOSE: Undo the decision
current.remove(choice);
}
}14.0.2.4 Key Implementation Details
1. Copying State When Storing Results
Always copy the state when adding to results:
// ❌ WRONG: Stores reference to mutable state
result.add(current);
// ✅ CORRECT: Stores snapshot of current state
result.add(new ArrayList<>(current));Without copying, all result entries reference the same object, which gets modified and eventually cleared.
2. Controlling Iteration to Avoid Duplicates
Use a starting index to maintain order and prevent duplicates:
// Prevents [1,2] and [2,1] from both appearing
for (int i = startIndex; i < choices.length; i++) {
current.add(choices[i]);
backtrack(current, i, result); // Pass 'i' or 'i+1'
current.remove(current.size() - 1);
}When to pass i vs i+1:
- Pass
i: Allow reusing the same choice (unbounded problems) - Pass
i+1: Each choice can only be used once (0/1 problems)
3. Pruning for Performance
Add early termination checks to avoid exploring doomed paths:
14.0.2.5 Common Backtracking Problems
| Problem Type | Key Variation | Example |
|---|---|---|
| Combination Sum | Unbounded reuse, pass i |
Find Index Combinations (HackerRank) |
| Subsets | Each element used once, pass i+1 |
Subsets (LeetCode 78) |
| Permutations | Each element used once, track used | Permutations (LeetCode 46) |
| N-Queens | Constraint checking, board state | N-Queens (LeetCode 51) |
| Word Search | 2D grid, mark visited | Word Search (LeetCode 79) |
14.0.2.6 Time and Space Complexity
Time Complexity: Typically \(O(b^d)\) where: - \(b\) is the branching factor (number of choices per level) - \(d\) is the maximum depth of recursion tree
This can be exponential, but pruning often reduces actual runtime significantly.
Space Complexity: \(O(d)\) for: - Recursion stack depth: \(O(d)\) - Current state storage: \(O(d)\) - Output storage: Not counted as auxiliary space
14.0.2.7 Example: Combination Sum with Unbounded Reuse
Problem: Find all combinations of indices whose weights sum to target (weights can be reused).
public List<List<Integer>> findCombinations(int[] weights, int target) {
List<List<Integer>> result = new ArrayList<>();
backtrack(weights, new ArrayList<>(), 0, target, result);
return result;
}
private void backtrack(int[] weights, List<Integer> current,
int index, int remaining, List<List<Integer>> result) {
// Base case: found valid combination
if (remaining == 0) {
result.add(new ArrayList<>(current)); // Copy!
return;
}
// Base case: pruning
if (index >= weights.length || remaining < 0) {
return;
}
// Try all choices starting from 'index'
for (int i = index; i < weights.length; i++) {
// 1. CHOOSE
current.add(i);
// 2. EXPLORE (pass 'i' for unbounded reuse)
backtrack(weights, current, i, remaining - weights[i], result);
// 3. UNCHOOSE
current.remove(current.size() - 1);
}
}Key observations:
- Passing
i(noti+1) allows reusing the same index - Loop starts from
indexto maintain non-decreasing order - Copying
currentis essential when storing inresult - Early return when
remaining == 0finds a valid solution - Early return when
remaining < 0prunes invalid paths
14.0.2.8 Pattern 2: Pass-by-Value (Immutable State)
Best for: Problems where creating new state objects is cleaner than manual undo operations, or when the state is small (e.g., integers, strings).
Core Concept: This pattern uses the same 2D traversal structure as Choose-Explore-Unchoose, but achieves backtracking through state copying instead of manual undo.
Instead of modifying and reverting state, create a new copy of the state for each recursive call. No unchoose step needed because each branch gets its own independent state.
2D Traversal Structure:
- Outer dimension: Still controlled by recursion parameter (
index,depth) - Inner dimension: Still controlled by for-loop over choices
- Key difference: State immutability replaces the unchoose step
The traversal pattern is identical to Pattern 1, only the state management differs (copy vs modify-revert).
Template:
public void backtrack(int[] choices, List<Integer> current, int index, Result result) {
// Base case: found valid solution
if (isValidSolution(current)) {
result.add(current); // Can add directly (already a copy)
return;
}
// Base case: pruning
if (shouldPrune(current, index)) {
return;
}
// Try all choices
for (int i = index; i < choices.length; i++) {
// Create NEW state by copying and adding choice
List<Integer> newState = new ArrayList<>(current);
newState.add(choices[i]);
// Explore with new state (no unchoose needed)
backtrack(choices, newState, i, result);
}
}Example: Permutations
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(nums, new ArrayList<>(), result);
return result;
}
private void backtrack(int[] nums, List<Integer> current, List<List<Integer>> result) {
// Found a complete permutation
if (current.size() == nums.length) {
result.add(current); // No copy needed!
return;
}
for (int num : nums) {
// Skip if already used
if (current.contains(num)) continue;
// Create new state with this choice
List<Integer> newCurrent = new ArrayList<>(current);
newCurrent.add(num);
// Explore (no unchoose step)
backtrack(nums, newCurrent, result);
}
}Pros:
- Simpler logic - no manual undo step
- No risk of forgetting to unchoose
- Easier to reason about (functional style)
Cons:
- Higher memory usage - creates new objects for each recursive call
- Slower for large state objects due to copying overhead
- Time: \(O(n)\) per call for copying state of size \(n\)
When to use:
- State is small (primitives, small strings, shallow copies)
- Code clarity is more important than memory efficiency
- Debugging backtracking issues (easier to trace)
14.0.2.9 Pattern 3: Mark-Unmark (Grid/Graph Traversal)
Best for: 2D grid or graph problems where you need to track visited cells/nodes to avoid cycles.
Core Concept: Mark cells/nodes as visited before exploring, then unmark them after to allow reuse in other paths. Similar to choose-explore-unchoose but uses in-place marking.
Template:
public void backtrack(char[][] grid, int row, int col, String current, Result result) {
// Base case: found valid solution
if (isValidSolution(current)) {
result.add(current);
return;
}
// Base case: out of bounds or already visited
if (row < 0 || row >= grid.length || col < 0 || col >= grid[0].length
|| grid[row][col] == '#') {
return;
}
// 1. MARK: Mark current cell as visited
char original = grid[row][col];
grid[row][col] = '#'; // Mark visited
// 2. EXPLORE: Try all 4 directions
backtrack(grid, row + 1, col, current + original, result); // Down
backtrack(grid, row - 1, col, current + original, result); // Up
backtrack(grid, row, col + 1, current + original, result); // Right
backtrack(grid, row, col - 1, current + original, result); // Left
// 3. UNMARK: Restore original value
grid[row][col] = original;
}Example: Word Search (LeetCode 79)
public boolean exist(char[][] board, String word) {
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (backtrack(board, word, 0, i, j)) {
return true;
}
}
}
return false;
}
private boolean backtrack(char[][] board, String word, int index, int row, int col) {
// Found complete word
if (index == word.length()) {
return true;
}
// Out of bounds or wrong character or already visited
if (row < 0 || row >= board.length || col < 0 || col >= board[0].length
|| board[row][col] != word.charAt(index)) {
return false;
}
// 1. MARK: Save and mark as visited
char temp = board[row][col];
board[row][col] = '#';
// 2. EXPLORE: Try all 4 directions
boolean found = backtrack(board, word, index + 1, row + 1, col) ||
backtrack(board, word, index + 1, row - 1, col) ||
backtrack(board, word, index + 1, row, col + 1) ||
backtrack(board, word, index + 1, row, col - 1);
// 3. UNMARK: Restore original character
board[row][col] = temp;
return found;
}Key Points:
- Use a sentinel value (like
#or*) to mark visited cells - Must restore original value to allow reuse in other paths
- Often combined with directional arrays:
int[][] dirs = {{0,1}, {1,0}, {0,-1}, {-1,0}}
Alternative: Separate Visited Array
Instead of modifying the grid, use a separate boolean array:
boolean[][] visited = new boolean[rows][cols];
// Mark
visited[row][col] = true;
// Explore...
// Unmark
visited[row][col] = false;When to use:
- 2D grid traversal (Word Search, Number of Islands with paths)
- Graph cycle detection in DFS
- Problems requiring path tracking (not just reachability)
14.0.2.10 Comparison of Backtracking Patterns
| Pattern | State Management | Memory | Code Complexity | Best Use Case |
|---|---|---|---|---|
| Choose-Explore-Unchoose | Mutable, shared | Low \(O(d)\) | Medium (must remember unchoose) | Combinations, subsets, partition problems |
| Pass-by-Value | Immutable, copied | High \(O(d \times n)\) | Low (no unchoose) | Small state, permutations, string building |
| Mark-Unmark | In-place marking | Low \(O(1)\) | Medium (grid manipulation) | 2D grid traversal, graph paths |
Key Takeaway: Choose the pattern based on: 1. State size: Small → Pass-by-Value, Large → Choose-Explore-Unchoose 2. Problem type: Grid/Graph → Mark-Unmark, Combinations → Choose-Explore-Unchoose 3. Code clarity vs. performance: Debugging → Pass-by-Value, Production → Choose-Explore-Unchoose