This lesson on Recursion & Backtracking is hands-on and example-driven. You will master a systematic 5-step framework to decompose algorithmic problems into recursive relations and base cases. You will formulate linear recurrences, solve multi-variable problems like grid path calculations, and evaluate call stack execution using inductive reasoning.
What You'll Be Able To Do
- Decompose complex recursive problems using the 5-step problem-solving framework.
- Formulate explicit base cases and recurrence relations for single-variable inputs.
- Trace call stack execution and state transitions across recursive function frames.
- Derive multi-variable recursive relations for two-dimensional grid navigation.
- Structure combinatorial decision trees for multi-branch partition problems.
Detailed Concept Walkthrough
1. The 5-Step Recursive Framework
Recursion breaks complex problems down into identical subproblems solved on smaller input sizes. The 5-step framework provides a repeatable engineering process to transition from a problem description to code.
- Mechanism: Identify the simplest possible valid input to define your base case without computation.
- Execution Flow: Play through small, concrete examples to visualize state progression and boundaries.
- Best Practice: Relate larger test cases to slightly smaller subproblems (e.g., relating $f(n)$ to $f(n-1)$) before generalizing the mathematical pattern into code.
# 5-Step Framework pattern:
# 1. Base case: simplest input
# 2. Relate: f(n) to f(n-1)
# 3. Generalize & implement
def solve_recursive(n):
if n <= 0: # Step 1: Base case
return 0
return n + solve_recursive(n - 1) # Steps 2-5: Recurrence
Key Takeaway: Always define the simplest non-recursive base case before deriving the generalized recursive relation.
2. Base Cases and Linear Recurrence
A linear recursive function reduces a single state variable toward a termination boundary at each step. Without an explicit base case, the function causes unbounded recursive descent.
- Mechanism: A base case acts as the stopping criterion that returns an explicit constant value directly without making further recursive calls.
- Under the Hood: Every recursive call pushes an activation record containing local variables and return addresses onto the system call stack until a base case returns.
- Syntax Rule: The base case condition must be evaluated at the top of the function body prior to triggering recursive calls.
def sum_to_n(n):
# Base case: explicitly defined identity
if n == 0:
return 0
# Recurrence relation: Sum(n) = n + Sum(n-1)
return n + sum_to_n(n - 1)
Key Takeaway: Every valid recursive function must decrement state toward an explicit base condition to avoid call stack overflow.
3. Call Stack Mechanics and Inductive Leap
Designing recursion requires trusting that sub-calls correctly compute simpler subproblems without tracing every execution branch. This mental abstraction is known as the recursive leap of faith.
- Execution Flow: Calls unwind in Last-In, First-Out (LIFO) order as stack frames resolve their return values back to their callers.
- Under the Hood: If $n$ stack frames are opened simultaneously, the algorithm incurs an $O(n)$ auxiliary space overhead on the call stack.
- Best Practice: Rely on inductive reasoning—assume $f(k)$ works for smaller values of $k$ when writing the logic for $f(k+1)$.
# Tracing sum_to_n(3):
# Frame 1: sum_to_n(3) -> waits on sum_to_n(2)
# Frame 2: sum_to_n(2) -> waits on sum_to_n(1)
# Frame 3: sum_to_n(1) -> waits on sum_to_n(0)
# Frame 4: sum_to_n(0) -> returns 0 (unwinding starts)
result = sum_to_n(3) # Evaluates to 3 + 2 + 1 + 0 = 6
Key Takeaway: Focus on the correctness of the single-step reduction rather than manually simulating the entire execution stack.
4. Multi-Variable Recursive State
Problems involving multi-dimensional spaces or multiple constraints require recursive relations that accept multiple inputs and branch into separate subproblems.
- Mechanism: In a 2D grid, reaching cell $(n, m)$ requires summing paths from the top cell $(n-1, m)$ and the left cell $(n, m-1)$.
- Execution Flow: Base cases must account for all dimensions reaching their terminal states, such as $n=1$ or $m=1$ yielding exactly 1 unique path.
- Under the Hood: Multi-branching recursion produces a binary tree of activation frames, creating an exponential time complexity if subproblems overlap.
def unique_paths(n, m):
# Base case: 1D line has only 1 direct path
if n == 1 or m == 1:
return 1
# Recurrence: sum of down and right paths
return unique_paths(n - 1, m) + unique_paths(n, m - 1)
Key Takeaway: Multi-variable recursive functions require comprehensive boundary conditions for each independent parameter.
Topics Covered in Recursion & Backtracking
- Five-Step Recursive Framework (0:06 - 0:41) — Introduces the five-step systematic framework for decomposing algorithmic problems into recursive functions.
- Linear Base Cases and Summation (0:43 - 3:00) — Demonstrates base case definition and mathematical recurrence relation derivation for linear summation.
- Call Stack Flow and Induction (3:01 - 4:36) — Traces stack frame execution order and explains inductive reasoning via the recursive leap of faith.
- Multi-Variable Grid Recursion (4:38 - 8:30) — Applies the framework to derive two-variable base cases and branching relations for unique grid paths.
- Combinatorial Partition Overview (8:34 - 9:07) — Analyzes problem inputs and state constraints for complex decision-based integer partitions.
DSA for Interviews Cheat Sheet
-
if n == 0: return 0— Linear base case stopping conditionif n == 0: return 0 -
return n + f(n - 1)— Linear recurrence step for cumulative summationreturn n + sum_n(n - 1) -
if n == 1 or m == 1: return 1— Multi-variable boundary condition for grid navigationif n == 1 or m == 1: return 1 -
return f(n - 1, m) + f(n, m - 1)— Two-branch recursive step combining orthogonal pathsreturn paths(n - 1, m) + paths(n, m - 1) -
partition(n, m)— Decomposes combinatorial integer partition subproblemsreturn partition(n - m, m) + partition(n, m - 1)
Comparison Table
| Feature | Linear Recursion (Sum) | Multi-Variable Recursion (Paths) |
|---|---|---|
| State Variables | Single parameter (n) | Multiple parameters (n, m) |
| Base Case Check | Single boundary (n == 0) | Multi-boundary (n == 1 or m == 1) |
| Recursive Calls | One call per frame | Two or more calls per frame |
| Call Tree Shape | Linear chain | Branching tree |
Common Pitfalls
- Mistake: Omitting boundary checks for one parameter in multi-variable recursion. Avoid: Define explicit base conditions covering terminal states for every input parameter.
- Mistake: Manually tracing deep recursion trees during code design. Avoid: Apply the recursive leap of faith by assuming subproblems return valid values.
- Mistake: Decrementing state without approaching the base case. Avoid: Ensure every recursive invocation strictly reduces input variables toward termination conditions.
FAQs
- What is the recursive leap of faith? It is assuming sub-calls already return correct results for smaller inputs, allowing you to focus strictly on combining them.
- Why do unique paths reduce to n == 1 or m == 1 as base cases? A grid with only one row or column allows only a single straight-line path to the destination.
- When does multi-variable recursion require Dynamic Programming? When recursive branching calculates identical state tuples multiple times across different branches of the call tree.
Backtracking: Course Supplement
The existing video covers general recursive decomposition. This companion applies recursion to search spaces by making a choice, exploring it, then undoing it.
State, choices, constraints, and undo
At each recursive call, define the current partial solution (state), the remaining choices, and the rule that says whether a branch can continue. Add one choice, recurse, and restore the state before exploring the next choice. A correct undo restores exactly the state that existed before the branch.
def subsets(nums):
result = []
path = []
def visit(start):
result.append(path.copy())
for i in range(start, len(nums)):
path.append(nums[i])
visit(i + 1)
path.pop() # undo this branch's choice
visit(0)
return result
The recursion tree has one branch per include/skip choice, so producing all subsets requires Θ(N·2^N) time when output copying is counted and Θ(N) call/path space apart from output.
Pruning and common patterns
- Permutations: track used positions; each path has no repeated index.
- Combinations: pass a start index so choices are selected in increasing index order and duplicates are not generated by ordering alone.
- Constraint search: stop a branch as soon as partial state cannot be extended to a valid result (for example, a row/column/diagonal conflict in N-Queens).
- Combination sum: when values are positive and the partial sum already exceeds the target, prune; this is not valid when future values can be negative.
Pruning must be logically sound: every pruned branch must be unable to contain a valid answer. Define duplicate handling, whether an empty result is valid, and whether the result contains all solutions or only one. If the maximum depth can exceed Python's recursion limit, use an explicit stack or state an input bound.