DSA DAY ONE
DSA DAY ONE
PART I — FOUNDATIONS
Chapter 1: C++ Essentials for DSA
Chapter 2: C++ STL Deep Dive
Chapter 3: Complexity Analysis
PART II — LINEAR DATA STRUCTURES
Chapter 4: Arrays & Strings
Chapter 5: Two Pointers & Sliding Window
Chapter 6: Hashing
Chapter 7: Linked Lists
Chapter 8: Stacks
Chapter 9: Queues & Deques
PART III — TREES
Chapter 10: Binary Trees
Chapter 11: Binary Search Trees (BST)
Chapter 12: Heaps & Priority Queues
Chapter 13: Tries (Prefix Trees)
Chapter 14: Segment Tree & Fenwick Tree
PART IV — ALGORITHMS
Chapter 15: Sorting Algorithms
Chapter 16: Binary Search
Chapter 17: Recursion & Backtracking
Recursion Fundamentals & Call Stack VisualizationRecursion Tree & Complexity AnalysisMemoization — Making Recursion FastBacktracking Framework — Template & PhilosophySubsets — Power Set GenerationPermutations & CombinationsN-Queens ProblemSudoku SolverWord Search on GridPruning StrategiesSample ProblemsLeetCode Problem Set
Chapter 18: Graphs — Fundamentals & Traversals
Chapter 19: Graph Algorithms — Shortest Paths, MST & Union-Find
Chapter 20: Dynamic Programming
Chapter 21: Greedy Algorithms
Chapter 22: Divide & Conquer
PART V — COMPETITIVE & MATH
Chapter 23: Bit Manipulation
Chapter 24: Math & Number Theory
PART VI — PATTERNS
Chapter 25: Pattern Recognition Mastery
PART VII — SYSTEM DESIGN
Chapter 26: System Design for FAANG Interviews
PART VIII — INTERVIEW STRATEGY
Chapter 27: Cracking FAANG — Strategy & Communication
17

Recursion & Backtracking


Chapter 17 ·  Part 4: ALGORITHMS ·  Est. 26 min read

Chapter Goal: Build a deep, intuitive understanding of recursion and master the backtracking framework that solves constraint-satisfaction problems. Backtracking is the systematic exploration of all possible solutions via a decision tree — pruning branches that can never lead to valid answers. Subsets, permutations, N-Queens, Sudoku, and word search all reduce to the same three-step loop: Choose → Explore → Unchoose.


17.1 Recursion Fundamentals & Call Stack Visualization

The Two Requirements of Every Recursive Function

  1. Base case: The stopping condition — a case so simple it can be solved directly.
  2. Recursive case: A smaller version of the same problem that converges toward the base case.
// Factorial: n! = n × (n-1) × ... × 1
int factorial(int n) {
    if (n <= 1) return 1;           // Base case: 0! = 1! = 1
    return n * factorial(n - 1);   // Recursive case: n × (n-1)!
}
 
// Fibonacci: fib(n) = fib(n-1) + fib(n-2)
int fib(int n) {
    if (n <= 1) return n;           // Base cases: fib(0)=0, fib(1)=1
    return fib(n-1) + fib(n-2);    // Recursive case
}

The Call Stack — What Happens at Runtime

factorial(4) call stack (grows down, unwinds up):

factorial(4) → needs factorial(3)
  factorial(3) → needs factorial(2)
    factorial(2) → needs factorial(1)
      factorial(1) → returns 1 (BASE CASE)
    factorial(2) = 2 × 1 = 2 (returns)
  factorial(3) = 3 × 2 = 6 (returns)
factorial(4) = 4 × 6 = 24 (returns)

Stack memory used: 4 frames (one per call)
Stack space: O(n) for depth-n recursion

The Leap of Faith — Trust Your Recursion

The hardest part of recursion is trusting that the recursive call works correctly. Assume the recursive call returns the correct answer for a smaller input, then build the answer for the current input.

// "Given that reverseList(head->next) correctly reverses the rest,
//  how do I make the full list reversed?"
ListNode* reverseList(ListNode* head) {
    if (!head || !head->next) return head;    // Base case
    ListNode* reversed = reverseList(head->next);  // Trust this works
    head->next->next = head;  // Now make current head the new tail
    head->next = nullptr;
    return reversed;
}

Common Recursion Mistakes

// ❌ Missing base case → infinite recursion → stack overflow
int f(int n) { return n * f(n-1); }   // Never stops!
 
// ❌ Base case that doesn't reduce correctly
int f(int n) {
    if (n == 0) return 0;
    return f(n);   // Not n-1, doesn't converge!
}
 
// ❌ Off-by-one in base case
int f(int n) {
    if (n == 1) return 1;   // What about n=0? Causes issues for some inputs
    return n * f(n-1);
}
 
// ✅ Correct
int f(int n) {
    if (n <= 0) return 1;   // Safe base case covers 0 and negative
    return n * f(n-1);
}

17.2 Recursion Tree & Complexity Analysis

Drawing the Recursion Tree

Each node represents a function call. Edges represent recursive calls made. The tree structure reveals both time and space complexity.

fib(4) recursion tree:

              fib(4)
             /       \
         fib(3)       fib(2)
         /    \       /    \
      fib(2) fib(1) fib(1) fib(0)
      /   \
  fib(1) fib(0)

Total calls: 9 nodes → O(2^n) time
Max depth:   4 levels → O(n) space (call stack)

Counting Nodes to Get Time Complexity

Rule: Time = (# nodes in recursion tree) × (work per node)

For fib(n): ~2^n nodes × O(1) work = O(2^n)
For mergeSort(n): ~2n nodes × O(n/level) = O(n log n)
For factorial(n): n nodes × O(1) work = O(n)

Space Complexity — Depth of the Tree

Recursion depth = maximum stack frames alive simultaneously
               = depth of the recursion tree

factorial(n):   depth n   → O(n) space
binarySearch:   depth log n → O(log n) space
fib(n):         depth n   → O(n) space (even though tree has 2^n nodes)
mergeSort:      depth log n → O(log n) space (O(n) for merge buffers)

Key Recursion Tree Patterns

Pattern Recurrence Time Space Example
Linear recursion T(n) = T(n-1) + O(1) O(n) O(n) Factorial
Binary recursion T(n) = 2T(n-1) + O(1) O(2^n) O(n) Naive Fibonacci
Divide & conquer T(n) = 2T(n/2) + O(n) O(n log n) O(log n) Merge Sort
Binary search T(n) = T(n/2) + O(1) O(log n) O(log n) Binary Search

17.3 Memoization — Making Recursion Fast

The Problem: Overlapping Subproblems

fib(5) computes fib(3) TWICE, fib(2) THREE TIMES, etc.
Memoization: store results so each unique call is computed only once.

Memoization Template

// Generic memoization with unordered_map
unordered_map<int, int> memo;
 
int fib(int n) {
    if (n <= 1) return n;
    if (memo.count(n)) return memo[n];   // Cache hit → return immediately
    memo[n] = fib(n-1) + fib(n-2);      // Cache miss → compute and store
    return memo[n];
}
// Time: O(n) — each unique n computed once
// Space: O(n) — memo map + O(n) call stack
 
// With vector (faster for bounded n):
vector<int> dp(n+1, -1);
int fib(int n) {
    if (n <= 1) return n;
    if (dp[n] != -1) return dp[n];
    return dp[n] = fib(n-1) + fib(n-2);
}

Memoization on Multi-Parameter Functions

// Longest Common Subsequence with memoization
string s, t;
vector<vector<int>> memo;
 
int lcs(int i, int j) {
    if (i < 0 || j < 0) return 0;
    if (memo[i][j] != -1) return memo[i][j];
 
    if (s[i] == t[j])
        return memo[i][j] = 1 + lcs(i-1, j-1);
    return memo[i][j] = max(lcs(i-1, j), lcs(i, j-1));
}
 
// Call: memo.assign(n, vector<int>(m, -1)); lcs(n-1, m-1);

Memoization vs. Tabulation (Bottom-Up DP)

Aspect Memoization (Top-Down) Tabulation (Bottom-Up)
Direction Problem → Subproblems Subproblems → Problem
Implementation Recursive + cache Iterative array
Stack overhead O(depth) call stack None
Solves only needed states Yes Usually all states
Easier to implement Yes Sometimes harder
Preferred for Complex state spaces Performance critical

17.4 Backtracking Framework — Template & Philosophy

What Is Backtracking?

Backtracking is a systematic search through all possible solutions by building them incrementally and abandoning ("backtracking") partial solutions as soon as it's determined they cannot lead to a valid complete solution.

Decision Tree for generating subsets of [1, 2, 3]:

                    []
                /          \
           [1]               []
          /    \           /     \
       [1,2]   [1]      [2]       []
       /  \    /  \    /   \    /    \
   [1,2,3][1,2][1,3][1] [2,3][2] [3]  []
      ↑     ↑    ↑   ↑   ↑   ↑   ↑   ↑
   (leaves = complete solutions = all 8 subsets)

The Three-Step Pattern

For every recursive call:

1. CHOOSE  — make a decision (include element, place queen, assign digit...)
2. EXPLORE — recurse on the smaller subproblem with this choice
3. UNCHOOSE — undo the decision ("backtrack") to explore other options

The Universal Backtracking Template

void backtrack(
    int       start,      // Current position in decision tree
    vector<T>& current,   // Current partial solution (state)
    vector<vector<T>>& result,  // All complete solutions found
    /* problem-specific parameters */
) {
    // 1. BASE CASE: Is the current state a complete solution?
    if (isComplete(current)) {
        result.push_back(current);
        return;
    }
 
    // 2. EXPLORE: Try each possible choice from current state
    for (int i = start; i < choices.size(); i++) {
        // 3. PRUNE: Skip invalid choices early
        if (!isValid(i, current)) continue;
 
        // 4. CHOOSE
        current.push_back(choices[i]);
 
        // 5. EXPLORE (recurse with choice made)
        backtrack(i + 1, current, result, /* ... */);
        //      or i (if repetition allowed)
 
        // 6. UNCHOOSE (undo to try next choice)
        current.pop_back();
    }
}

When to Use Backtracking

The problem requires enumerating:
  → All subsets / power set
  → All permutations
  → All valid combinations (with constraints)
  → All solutions to a constraint-satisfaction problem

Key signals:
  "Find ALL solutions..." → backtracking
  "Generate all..." → backtracking
  "Is there a valid arrangement..." → backtracking + return early on first found
  n ≤ 20 (or values that suggest exponential time is OK) → backtracking

17.5 Subsets — Power Set Generation

Approach 1: Include / Exclude (Recursion)

At each index, either include or exclude the element.

void backtrack(vector<int>& nums, int start, vector<int>& curr,
               vector<vector<int>>& result) {
    result.push_back(curr);   // Every partial state IS a valid subset
 
    for (int i = start; i < (int)nums.size(); i++) {
        curr.push_back(nums[i]);     // Choose: include nums[i]
        backtrack(nums, i+1, curr, result);  // Explore
        curr.pop_back();             // Unchoose
    }
}
 
vector<vector<int>> subsets(vector<int>& nums) {
    vector<vector<int>> result;
    vector<int> curr;
    backtrack(nums, 0, curr, result);
    return result;
}
// Time: O(2^n × n) — 2^n subsets, each copied in O(n)
// Space: O(n) — recursion depth + curr array

Approach 2: Bitmask Enumeration

vector<vector<int>> subsets(vector<int>& nums) {
    int n = nums.size();
    vector<vector<int>> result;
 
    for (int mask = 0; mask < (1 << n); mask++) {
        vector<int> subset;
        for (int i = 0; i < n; i++) {
            if (mask & (1 << i)) subset.push_back(nums[i]);
        }
        result.push_back(subset);
    }
    return result;
}
// Time: O(2^n × n), Space: O(n) extra
// Best for n ≤ 20 (fits in int) or n ≤ 30 (fits in long long)

Subsets with Duplicates (LC 90)

Key rule: Sort first. Skip the same element at the same recursion level (i > start, not i > 0 — this distinction is critical).

void backtrack(vector<int>& nums, int start, vector<int>& curr,
               vector<vector<int>>& result) {
    result.push_back(curr);
 
    for (int i = start; i < (int)nums.size(); i++) {
        // Skip duplicates at the SAME recursion level
        if (i > start && nums[i] == nums[i-1]) continue;
 
        curr.push_back(nums[i]);
        backtrack(nums, i+1, curr, result);
        curr.pop_back();
    }
}
 
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
    sort(nums.begin(), nums.end());   // Sort first!
    vector<vector<int>> result;
    vector<int> curr;
    backtrack(nums, 0, curr, result);
    return result;
}
/*
  Why i > start (not i > 0)?
  nums = [1, 2, 2], sorted
  At level 0 (start=0): i=1 (pick first 2), i=2 skip (same as nums[1]=2)
    → [2] is generated once
  At level 1 (start=1, already picked first 2): i=2 (pick second 2)
    → [2,2] is generated (fine, different subset)
  If we used i > 0: we'd skip the second 2 even when starting fresh
*/

17.6 Permutations & Combinations

Permutations of Unique Elements (LC 46)

void backtrack(vector<int>& nums, vector<bool>& used,
               vector<int>& curr, vector<vector<int>>& result) {
    if (curr.size() == nums.size()) {
        result.push_back(curr);
        return;
    }
    for (int i = 0; i < (int)nums.size(); i++) {
        if (used[i]) continue;        // Skip already-used elements
        used[i] = true;               // Choose
        curr.push_back(nums[i]);
        backtrack(nums, used, curr, result);  // Explore
        curr.pop_back();              // Unchoose
        used[i] = false;
    }
}
 
vector<vector<int>> permute(vector<int>& nums) {
    vector<vector<int>> result;
    vector<bool> used(nums.size(), false);
    vector<int> curr;
    backtrack(nums, used, curr, result);
    return result;
}
// Time: O(n! × n), Space: O(n)

Permutations with Duplicates (LC 47)

void backtrack(vector<int>& nums, vector<bool>& used,
               vector<int>& curr, vector<vector<int>>& result) {
    if (curr.size() == nums.size()) { result.push_back(curr); return; }
 
    for (int i = 0; i < (int)nums.size(); i++) {
        if (used[i]) continue;
        // Skip: same value AND previous same value was NOT used
        // (if prev was used, current would be in a different position → different perm)
        if (i > 0 && nums[i] == nums[i-1] && !used[i-1]) continue;
 
        used[i] = true;
        curr.push_back(nums[i]);
        backtrack(nums, used, curr, result);
        curr.pop_back();
        used[i] = false;
    }
}
 
vector<vector<int>> permuteUnique(vector<int>& nums) {
    sort(nums.begin(), nums.end());
    vector<vector<int>> result;
    vector<bool> used(nums.size(), false);
    vector<int> curr;
    backtrack(nums, used, curr, result);
    return result;
}
/*
  Why !used[i-1] (not used[i-1])?
  nums = [1, 1, 2]
  When generating [1(a), 1(b), 2]:
    i=0 used → i=1 both 1s valid if we don't skip
  When generating [1(b), 1(a), 2]:
    i=0 (first 1) skip because nums[0]==nums[-1]? No, i=0 has no prev.
  The condition !used[i-1] means:
    "If the previous same element was NOT chosen before the current element,
     we'd be generating the same permutation (just with a different choice order)."
*/

Combination Sum — Reuse Elements Allowed (LC 39)

void backtrack(vector<int>& candidates, int target, int start,
               vector<int>& curr, vector<vector<int>>& result) {
    if (target == 0) { result.push_back(curr); return; }
    if (target < 0) return;   // Prune: exceeded target
 
    for (int i = start; i < (int)candidates.size(); i++) {
        curr.push_back(candidates[i]);
        backtrack(candidates, target - candidates[i], i, curr, result);
        //                                              ↑ i not i+1 (allow reuse)
        curr.pop_back();
    }
}
 
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
    sort(candidates.begin(), candidates.end());
    vector<vector<int>> result;
    vector<int> curr;
    backtrack(candidates, target, 0, curr, result);
    return result;
}
// Time: O(N^(T/M)) where N=candidates, T=target, M=minimum candidate

Combination Sum II — No Reuse, Duplicates in Input (LC 40)

void backtrack(vector<int>& candidates, int target, int start,
               vector<int>& curr, vector<vector<int>>& result) {
    if (target == 0) { result.push_back(curr); return; }
 
    for (int i = start; i < (int)candidates.size(); i++) {
        if (candidates[i] > target) break;  // Pruned: sorted, rest are too large
        if (i > start && candidates[i] == candidates[i-1]) continue;  // Skip duplicates
 
        curr.push_back(candidates[i]);
        backtrack(candidates, target - candidates[i], i+1, curr, result);
        //                                               ↑ i+1 (no reuse)
        curr.pop_back();
    }
}

Generate Parentheses (LC 22)

void backtrack(int open, int close, int n, string& curr, vector<string>& result) {
    if (curr.size() == 2*n) { result.push_back(curr); return; }
 
    if (open < n) {
        curr += '(';
        backtrack(open+1, close, n, curr, result);
        curr.pop_back();
    }
    if (close < open) {   // Can only close if we have unclosed opens
        curr += ')';
        backtrack(open, close+1, n, curr, result);
        curr.pop_back();
    }
}
 
vector<string> generateParenthesis(int n) {
    vector<string> result;
    string curr;
    backtrack(0, 0, n, curr, result);
    return result;
}
// Time: O(4^n / sqrt(n)) — Catalan number
// The pruning (close < open) eliminates all invalid states

17.7 N-Queens Problem

Problem Definition

Place n queens on an n×n chessboard so no two queens share the same row, column, or diagonal.

Key Insight — Three Attack Vectors

For a queen at (row, col):
  Same column: col is used
  Left diagonal (\): row - col is constant for the entire diagonal
  Right diagonal (/): row + col is constant for the entire diagonal

Track three sets:
  cols      — which columns have a queen
  diag1     — which (row-col) diagonals have a queen
  diag2     — which (row+col) diagonals have a queen

Placing is valid when all three sets don't contain the queen's coordinates.
class NQueens {
    int n;
    vector<bool> cols, diag1, diag2;  // Attack trackers
 
    void backtrack(int row, vector<string>& board, vector<vector<string>>& result) {
        if (row == n) {
            result.push_back(board);   // All rows placed → complete solution
            return;
        }
        for (int col = 0; col < n; col++) {
            // Check if this cell is under attack
            if (cols[col] || diag1[row-col+n] || diag2[row+col]) continue;
 
            // Place queen
            board[row][col] = 'Q';
            cols[col] = diag1[row-col+n] = diag2[row+col] = true;
 
            backtrack(row+1, board, result);
 
            // Remove queen
            board[row][col] = '.';
            cols[col] = diag1[row-col+n] = diag2[row+col] = false;
        }
    }
 
public:
    vector<vector<string>> solveNQueens(int n) {
        this->n = n;
        cols.assign(n, false);
        diag1.assign(2*n, false);   // row-col ranges from -(n-1) to n-1, offset by n
        diag2.assign(2*n, false);   // row+col ranges from 0 to 2(n-1)
 
        vector<vector<string>> result;
        vector<string> board(n, string(n, '.'));
        backtrack(0, board, result);
        return result;
    }
};
// Time: O(n!) — at most n! arrangements to check
// Space: O(n) — recursion depth n + O(n) for trackers

Count Only (No Board Construction) — O(n!) Optimized

int totalNQueens(int n) {
    vector<bool> cols(n), diag1(2*n), diag2(2*n);
    int count = 0;
 
    function<void(int)> bt = [&](int row) {
        if (row == n) { count++; return; }
        for (int col = 0; col < n; col++) {
            if (cols[col] || diag1[row-col+n] || diag2[row+col]) continue;
            cols[col] = diag1[row-col+n] = diag2[row+col] = true;
            bt(row+1);
            cols[col] = diag1[row-col+n] = diag2[row+col] = false;
        }
    };
    bt(0);
    return count;
}

17.8 Sudoku Solver

Approach — Constraint Propagation via Backtracking

For each empty cell, try digits 1–9. Check validity against row, column, and 3×3 box. Backtrack if no digit works.

class SudokuSolver {
    bool rows[9][10]  = {};  // rows[r][d]: digit d used in row r
    bool cols[9][10]  = {};  // cols[c][d]: digit d used in col c
    bool boxes[9][10] = {};  // boxes[b][d]: digit d used in box b
 
    int boxIdx(int r, int c) { return (r/3)*3 + c/3; }
 
    bool solve(vector<vector<char>>& board) {
        // Find next empty cell
        for (int r = 0; r < 9; r++) {
            for (int c = 0; c < 9; c++) {
                if (board[r][c] != '.') continue;
 
                // Try each digit 1-9
                for (int d = 1; d <= 9; d++) {
                    if (rows[r][d] || cols[c][d] || boxes[boxIdx(r,c)][d]) continue;
 
                    // Place digit
                    board[r][c] = '0' + d;
                    rows[r][d] = cols[c][d] = boxes[boxIdx(r,c)][d] = true;
 
                    if (solve(board)) return true;  // Solution found!
 
                    // Remove digit (backtrack)
                    board[r][c] = '.';
                    rows[r][d] = cols[c][d] = boxes[boxIdx(r,c)][d] = false;
                }
                return false;  // No valid digit → backtrack further
            }
        }
        return true;  // No empty cell found → board is complete
    }
 
public:
    void solveSudoku(vector<vector<char>>& board) {
        // Pre-fill constraints from given board
        for (int r = 0; r < 9; r++) {
            for (int c = 0; c < 9; c++) {
                if (board[r][c] == '.') continue;
                int d = board[r][c] - '0';
                rows[r][d] = cols[c][d] = boxes[boxIdx(r,c)][d] = true;
            }
        }
        solve(board);
    }
};
// Time: O(9^m) where m = number of empty cells (≤ 81)
// In practice much faster due to heavy pruning from row/col/box constraints

17.9 Word Search on Grid

Standard Word Search (LC 79)

bool wordSearch(vector<vector<char>>& board, string word) {
    int rows = board.size(), cols = board[0].size();
    int dx[] = {0,0,1,-1}, dy[] = {1,-1,0,0};
 
    function<bool(int,int,int)> dfs = [&](int r, int c, int k) -> bool {
        if (k == (int)word.size()) return true;   // All characters matched
        if (r<0||r>=rows||c<0||c>=cols) return false;
        if (board[r][c] != word[k]) return false;
 
        char saved = board[r][c];
        board[r][c] = '#';   // Mark as visited (in-place)
 
        for (int d = 0; d < 4; d++) {
            if (dfs(r+dx[d], c+dy[d], k+1)) {
                board[r][c] = saved;  // Restore before returning
                return true;
            }
        }
        board[r][c] = saved;   // Restore (backtrack)
        return false;
    };
 
    for (int r = 0; r < rows; r++)
        for (int c = 0; c < cols; c++)
            if (dfs(r, c, 0)) return true;
 
    return false;
}
// Time: O(rows×cols × 4 × 3^(L-1)) where L = word length
//   4 choices for first cell, 3 for each subsequent (can't go back)
// Space: O(L) — recursion depth

17.10 Pruning Strategies

Pruning eliminates branches of the decision tree that cannot lead to valid solutions. Effective pruning can reduce O(2^n) to O(n!) to much smaller.

Strategy 1 — Early Target Exceed

// In combination sum: if current element > remaining target, stop
for (int i = start; i < candidates.size(); i++) {
    if (candidates[i] > target) break;   // Sorted array → rest also exceed
    // ...
}

Strategy 2 — Duplicate Skipping

// At same recursion level, skip elements equal to the previous one
for (int i = start; i < nums.size(); i++) {
    if (i > start && nums[i] == nums[i-1]) continue;   // Skip duplicates
    // ...
}

Strategy 3 — Remaining Element Count Check

// If not enough elements remain to complete the solution, prune
void backtrack(int start, int k, vector<int>& curr, ...) {
    // Need k-curr.size() more elements; only n-start elements remain
    if ((int)nums.size() - start < k - (int)curr.size()) return;  // Pruned!
    if (curr.size() == k) { result.push_back(curr); return; }
    // ...
}

Strategy 4 — Constraint Propagation (Pre-computed Validity)

// For palindrome partitioning: precompute which substrings are palindromes
// O(n²) precomputation → O(1) palindrome check during backtracking
vector<vector<bool>> isPalin;
void precompute(const string& s) {
    int n = s.size();
    isPalin.assign(n, vector<bool>(n, false));
    for (int i = 0; i < n; i++) isPalin[i][i] = true;
    for (int len = 2; len <= n; len++) {
        for (int i = 0; i + len - 1 < n; i++) {
            int j = i + len - 1;
            isPalin[i][j] = (s[i] == s[j]) && (len == 2 || isPalin[i+1][j-1]);
        }
    }
}

Strategy 5 — Bounding Function (Branch and Bound)

// For TSP / assignment problems: if current partial cost > best complete solution, prune
if (currentCost + lowerBoundEstimate >= bestCost) return;  // Can't improve → prune

17.11 Sample Problems


Problem 1 — Subsets (Medium) — LC 78

vector<vector<int>> subsets(vector<int>& nums) {
    vector<vector<int>> result;
    vector<int> curr;
    function<void(int)> bt = [&](int start) {
        result.push_back(curr);
        for (int i = start; i < (int)nums.size(); i++) {
            curr.push_back(nums[i]);
            bt(i+1);
            curr.pop_back();
        }
    };
    bt(0);
    return result;
}
// Time: O(2^n × n), Space: O(n)

Problem 2 — Subsets II with Duplicates (Medium) — LC 90

vector<vector<int>> subsetsWithDup(vector<int>& nums) {
    sort(nums.begin(), nums.end());
    vector<vector<int>> result;
    vector<int> curr;
    function<void(int)> bt = [&](int start) {
        result.push_back(curr);
        for (int i = start; i < (int)nums.size(); i++) {
            if (i > start && nums[i] == nums[i-1]) continue;
            curr.push_back(nums[i]); bt(i+1); curr.pop_back();
        }
    };
    bt(0);
    return result;
}
// Time: O(2^n × n), Space: O(n)

Problem 3 — Permutations (Medium) — LC 46

vector<vector<int>> permute(vector<int>& nums) {
    vector<vector<int>> result;
    vector<bool> used(nums.size(), false);
    vector<int> curr;
    function<void()> bt = [&]() {
        if (curr.size() == nums.size()) { result.push_back(curr); return; }
        for (int i = 0; i < (int)nums.size(); i++) {
            if (used[i]) continue;
            used[i] = true; curr.push_back(nums[i]);
            bt();
            used[i] = false; curr.pop_back();
        }
    };
    bt();
    return result;
}
// Time: O(n! × n), Space: O(n)

Problem 4 — Permutations II with Duplicates (Medium) — LC 47

vector<vector<int>> permuteUnique(vector<int>& nums) {
    sort(nums.begin(), nums.end());
    vector<vector<int>> result;
    vector<bool> used(nums.size(), false);
    vector<int> curr;
    function<void()> bt = [&]() {
        if (curr.size() == nums.size()) { result.push_back(curr); return; }
        for (int i = 0; i < (int)nums.size(); i++) {
            if (used[i]) continue;
            if (i > 0 && nums[i] == nums[i-1] && !used[i-1]) continue;
            used[i] = true; curr.push_back(nums[i]);
            bt();
            used[i] = false; curr.pop_back();
        }
    };
    bt();
    return result;
}
// Time: O(n! × n), Space: O(n)

Problem 5 — Combination Sum (Medium) — LC 39

vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
    sort(candidates.begin(), candidates.end());
    vector<vector<int>> result;
    vector<int> curr;
    function<void(int,int)> bt = [&](int start, int rem) {
        if (rem == 0) { result.push_back(curr); return; }
        for (int i = start; i < (int)candidates.size(); i++) {
            if (candidates[i] > rem) break;
            curr.push_back(candidates[i]);
            bt(i, rem - candidates[i]);   // i (not i+1): allow reuse
            curr.pop_back();
        }
    };
    bt(0, target);
    return result;
}
// Time: O(N^(T/M)) roughly, Space: O(T/M) depth

Problem 6 — Generate Parentheses (Medium) — LC 22

vector<string> generateParenthesis(int n) {
    vector<string> result;
    string curr;
    function<void(int,int)> bt = [&](int open, int close) {
        if ((int)curr.size() == 2*n) { result.push_back(curr); return; }
        if (open < n)     { curr+='('; bt(open+1, close); curr.pop_back(); }
        if (close < open) { curr+=')'; bt(open, close+1); curr.pop_back(); }
    };
    bt(0, 0);
    return result;
}
// Time: O(4^n / sqrt(n)) — Catalan number, Space: O(n)

Problem 7 — Letter Combinations of a Phone Number (Medium) — LC 17

vector<string> letterCombinations(string digits) {
    if (digits.empty()) return {};
    vector<string> phoneMap = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
    vector<string> result;
    string curr;
    function<void(int)> bt = [&](int idx) {
        if (idx == (int)digits.size()) { result.push_back(curr); return; }
        for (char c : phoneMap[digits[idx]-'0']) {
            curr += c; bt(idx+1); curr.pop_back();
        }
    };
    bt(0);
    return result;
}
// Time: O(4^n × n) where n = digits.length, Space: O(n)

Problem 8 — Word Search (Medium) — LC 79

bool exist(vector<vector<char>>& board, string word) {
    int R=board.size(), C=board[0].size();
    int dx[]={0,0,1,-1}, dy[]={1,-1,0,0};
    function<bool(int,int,int)> dfs=[&](int r,int c,int k)->bool {
        if(k==(int)word.size()) return true;
        if(r<0||r>=R||c<0||c>=C||board[r][c]!=word[k]) return false;
        char sv=board[r][c]; board[r][c]='#';
        for(int d=0;d<4;d++) if(dfs(r+dx[d],c+dy[d],k+1)){board[r][c]=sv;return true;}
        board[r][c]=sv;
        return false;
    };
    for(int r=0;r<R;r++) for(int c=0;c<C;c++) if(dfs(r,c,0)) return true;
    return false;
}
// Time: O(R×C×3^L), Space: O(L) where L=word length

Problem 9 — Palindrome Partitioning (Medium) — LC 131

Partition a string such that every substring is a palindrome.

vector<vector<string>> partition(string s) {
    int n = s.size();
    // Precompute palindrome table
    vector<vector<bool>> isPalin(n, vector<bool>(n, false));
    for (int i = n-1; i >= 0; i--)
        for (int j = i; j < n; j++)
            isPalin[i][j] = (s[i]==s[j]) && (j-i<2 || isPalin[i+1][j-1]);
 
    vector<vector<string>> result;
    vector<string> curr;
    function<void(int)> bt = [&](int start) {
        if (start == n) { result.push_back(curr); return; }
        for (int end = start; end < n; end++) {
            if (!isPalin[start][end]) continue;
            curr.push_back(s.substr(start, end-start+1));
            bt(end+1);
            curr.pop_back();
        }
    };
    bt(0);
    return result;
}
// Time: O(2^n × n), Space: O(n²) for palindrome table

Problem 10 — N-Queens (Hard) — LC 51

vector<vector<string>> solveNQueens(int n) {
    vector<bool> cols(n), d1(2*n), d2(2*n);
    vector<vector<string>> result;
    vector<string> board(n, string(n,'.'));
 
    function<void(int)> bt = [&](int row) {
        if (row == n) { result.push_back(board); return; }
        for (int col = 0; col < n; col++) {
            if (cols[col]||d1[row-col+n]||d2[row+col]) continue;
            board[row][col]='Q';
            cols[col]=d1[row-col+n]=d2[row+col]=true;
            bt(row+1);
            board[row][col]='.';
            cols[col]=d1[row-col+n]=d2[row+col]=false;
        }
    };
    bt(0);
    return result;
}
// Time: O(n!), Space: O(n)

Problem 11 — Sudoku Solver (Hard) — LC 37

void solveSudoku(vector<vector<char>>& board) {
    bool rows[9][10]={}, cols[9][10]={}, boxes[9][10]={};
    auto box=[](int r,int c){ return (r/3)*3+c/3; };
 
    for(int r=0;r<9;r++) for(int c=0;c<9;c++) if(board[r][c]!='.') {
        int d=board[r][c]-'0';
        rows[r][d]=cols[c][d]=boxes[box(r,c)][d]=true;
    }
 
    function<bool()> solve=[&]()->bool {
        for(int r=0;r<9;r++) for(int c=0;c<9;c++) {
            if(board[r][c]!='.') continue;
            for(int d=1;d<=9;d++) {
                if(rows[r][d]||cols[c][d]||boxes[box(r,c)][d]) continue;
                board[r][c]='0'+d;
                rows[r][d]=cols[c][d]=boxes[box(r,c)][d]=true;
                if(solve()) return true;
                board[r][c]='.';
                rows[r][d]=cols[c][d]=boxes[box(r,c)][d]=false;
            }
            return false;
        }
        return true;
    };
    solve();
}
// Time: O(9^m) worst, fast in practice due to constraints

Problem 12 — Combination Sum III (Medium) — LC 216

Find all combinations of k numbers (1–9) that sum to n, each number used once.

vector<vector<int>> combinationSum3(int k, int n) {
    vector<vector<int>> result;
    vector<int> curr;
    function<void(int,int)> bt = [&](int start, int rem) {
        if (curr.size()==(size_t)k && rem==0) { result.push_back(curr); return; }
        if ((int)curr.size()==k || rem<=0) return;
        for (int i=start; i<=9; i++) {
            if (i > rem) break;   // Prune: rest also too large
            curr.push_back(i);
            bt(i+1, rem-i);
            curr.pop_back();
        }
    };
    bt(1, n);
    return result;
}
// Time: O(C(9,k) × k), Space: O(k)

17.12 LeetCode Problem Set

# Problem Difficulty Core Technique Link
1 Subsets Medium Include/exclude backtracking LC 78
2 Permutations Medium Used[] array backtracking LC 46
3 Combination Sum Medium Backtrack with reuse (pass i not i+1) LC 39
4 Generate Parentheses Medium Count-based pruning (close < open) LC 22
5 Word Search Medium DFS + in-place visited marking LC 79
6 Palindrome Partitioning Medium Backtrack + DP palindrome table LC 131
7 N-Queens Hard Row-by-row + diagonal set tracking LC 51
8 Sudoku Solver Hard Cell-by-cell + row/col/box sets LC 37

Suggested order: #1 (Subsets) — implement all three approaches (recursive, bitmask, BFS). Then #2 (Permutations) — the used[] pattern. Then #3 (Combination Sum) — understand i vs i+1 for reuse. Then #4 (Parentheses) — elegant count-based pruning. Then #5 (Word Search) — grid DFS with in-place marking. Then #6 (Palindrome Partitioning) — precomputation + backtracking. Save #7 and #8 for last — N-Queens and Sudoku are the purest backtracking benchmarks.


← PreviousChapter 16: Binary SearchNext →Chapter 18: Graphs — Fundamentals & Traversals
Chapter 17 — Progress
0 / 28 COMPLETE
Recursion Fundamentals
  • Every recursive function has a base case that stops recursion and a recursive case that reduces the problem
  • Know that recursion depth = stack space = O(depth) memory
  • Use the "leap of faith": assume recursive call is correct, build answer for current state
  • Know the overflow risk: depth > ~10,000 causes stack overflow → convert to iterative
Memoization
  • Add `if (memo.count(key)) return memo[key]` before recursive call
  • Know memoization converts O(2^n) fib to O(n) — each unique state computed once
  • Know when memo is an array vs unordered_map (bounded vs unbounded keys)
Backtracking Template
  • State the three steps: Choose, Explore, Unchoose
  • Write the universal backtracking template from memory
  • Know: call result.push_back(curr) when isComplete, then return
  • Know: call curr.pop_back() AFTER the recursive call (LIFO order)
Subsets
  • Implement include/exclude recursion (push curr at every level)
  • Implement bitmask enumeration (for n ≤ 20)
  • Handle duplicates: sort + `if (i > start && nums[i] == nums[i-1]) continue`
  • Know why `i > start` (not `i > 0`) — skip at same recursion level, not globally
Permutations & Combinations
  • Implement permutations using used[] array
  • Handle duplicate permutations: sort + `if (i>0 && nums[i]==nums[i-1] && !used[i-1]) continue`
  • Know i vs i+1 in recursive call: i allows reuse (combination sum), i+1 prevents it
  • Implement generate parentheses with (open < n) and (close < open) pruning
N-Queens & Sudoku
  • Track columns, left diagonals (row-col), right diagonals (row+col) for N-Queens
  • Know: left diagonal offset = row-col+n (to avoid negative indices)
  • Track rows[9][10], cols[9][10], boxes[9][10] for Sudoku
  • Return false when no digit works in a cell (trigger backtrack)
  • Return true when no empty cell found (board complete)
Pruning
  • Apply early break when sorted candidates exceed remaining target
  • Apply remaining-count check: prune when not enough elements left
  • Precompute palindrome table for O(1) check during palindrome partitioning
  • In N-Queens: use O(1) set lookup vs O(n) board scan for attack detection
Notes▸