Loading content...
In the previous pages, we used recursion trees to analyze complexity. We counted nodes and calculated work per level. But we glossed over something remarkable: many nodes in our trees compute exactly the same thing.
Look closely at a Fibonacci recursion tree and you'll see fib(3) appear multiple times. Each occurrence triggers an entire subtree of computation—all of which produces the same answer. This is redundant computation: doing unnecessary work because we forgot we already solved that subproblem.
Recursion trees make this waste visible. By spotting repeated subproblems in the tree, we identify exactly where redundancy occurs, how much time it costs, and how to eliminate it. This insight is the doorway to memoization and dynamic programming—techniques that transform exponential algorithms into polynomial ones.
By the end of this page, you will understand how to identify redundant computations using recursion trees, quantify the cost of redundancy, recognize the 'overlapping subproblems' property, and understand how memoization and dynamic programming exploit this structure to achieve dramatic speedups.
A redundant computation occurs when the same subproblem is solved multiple times during the execution of a recursive algorithm. In the recursion tree, redundancy appears as multiple nodes with identical parameters.
Key terminology:
Visual identification:
In a recursion tree, redundant computations appear as duplicate labels. If you see fib(3) in two different places, that's redundancy—the same calculation happens twice.
123456789101112131415161718192021
fib(5) / \ fib(4) fib(3) ━━━━━━━━━━┓ / \ / \ ┃ fib(3) fib(2) fib(2) fib(1) ┃ SAME! / \ ... ... ┃ fib(2) fib(1) ━━━━━━━━━━━━━━━━━━━━━━━━━┛ ... REDUNDANT COMPUTATIONS FOUND:━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━• fib(3) appears 2 times• fib(2) appears 3 times• fib(1) appears 5 times• fib(0) appears 3 times Each duplicate triggers its entire subtree:• fib(3) spawns fib(2) + fib(1), which spawn more...• Computing fib(3) twice means computing fib(2) twice just from that! The waste compounds exponentially with depth.It might seem like fib(2) appearing 3 times is minor. But each occurrence of fib(2) spawns fib(1) and fib(0). Each occurrence of fib(3) spawns fib(2) and fib(1). Redundancy at higher levels cascades down, multiplying waste exponentially. This is why naive Fibonacci is O(2ⁿ) instead of O(n).
The presence of redundant computations is formally called the overlapping subproblems property. This is one of the two key characteristics that indicate a problem can benefit from dynamic programming (the other being optimal substructure).
Definition:
A recursive algorithm has overlapping subproblems when different recursive calls encounter the same subproblem. In the recursion tree, this manifests as duplicate nodes.
Not all recursion has overlapping subproblems:
In merge sort, the left and right halves are DISJOINT—they share no elements. So merge_sort([1,2,3,4]) and merge_sort([5,6,7,8]) have nothing in common. In Fibonacci, fib(n-1) and fib(n-2) both need fib(n-3) and smaller—they OVERLAP. This overlap is what creates redundancy and what dynamic programming exploits.
How bad is the redundancy? We can measure it by comparing:
The ratio reveals how much waste exists.
Fibonacci Analysis:
123456789101112131415161718192021222324252627282930313233343536373839404142
UNIQUE SUBPROBLEMS:━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━For fib(n), the unique subproblems are: fib(0), fib(1), fib(2), ..., fib(n) Total unique subproblems: n + 1 Each requires O(1) work (just an addition).Necessary work: O(n) ACTUAL NODES IN TREE:━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━Total nodes: O(2ⁿ) as we computed earlier.This is exponential! REDUNDANCY FACTOR:━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━Actual work / Necessary work = O(2ⁿ) / O(n) = O(2ⁿ/n) For n = 40:• Unique subproblems: 41• Actual tree nodes: ~1.1 billion (2⁴⁰ ≈ 10¹²)• Redundancy factor: ~27 million! Each unique subproblem is solved, on average, TWENTY-SEVEN MILLION TIMES when n = 40! SPECIFIC COUNTS for fib(n):━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━How many times is fib(k) computed when calculating fib(n)? It turns out fib(k) is computed fib(n-k+1) times! For fib(10):• fib(1) is computed 55 times (= fib(10))• fib(2) is computed 34 times (= fib(9))• fib(3) is computed 21 times (= fib(8))• fib(4) is computed 13 times (= fib(7))• fib(5) is computed 8 times (= fib(6))The redundancy in naive Fibonacci isn't just inefficient—it's catastrophic. The algorithm does exponentially more work than necessary. A problem solvable in O(n) time takes O(2ⁿ) time instead. For n=50, that's the difference between 50 operations and a quadrillion operations.
To effectively identify redundancy, we need techniques for marking and tracking repeated subproblems in our trees. Here are practical approaches:
1234567891011121314151617181920212223242526
fib(5) [computed: 1x] / \ fib(4) fib(3) [computed: 2x] ←── REPEATED! [1x] / \ / \ fib(3) fib(2) fib(2) fib(1) [2x]↑ [3x]↑ [3x]↑ [5x]↑ \ └──────┘ ↑ \__________________|_______ All REPEATED! COLLAPSED DAG FORM (essential structure):━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ fib(5) ↙ ↘ fib(4) → fib(3) ↙↘ ↙↘ fib(3) fib(2) fib(1) ↘ ↙↘ ↙ fib(2) → fib(1) ↘ ↙ fib(0) In the DAG, each node appears ONCE.Edges show dependencies.Nodes: n + 1 = O(n)This IS the optimal complexity!When you collapse a recursion tree into a DAG, you see the UNIQUE subproblems and their dependencies. The DAG for Fibonacci has just n+1 nodes—this is exactly what dynamic programming computes. The tree has O(2ⁿ) nodes because it traverses the same DAG paths multiple times.
Understanding why redundancy occurs helps us recognize it in new problems. Redundancy arises from specific patterns in how recursive calls are structured.
Pattern 1: Multiple calls that converge
fib(n-1) and fib(n-2) both eventually need fib(n-3). The paths converge.
Pattern 2: Order-independent subproblems
Problems where the order of choices doesn't affect the remaining subproblem. In coin change, whether we first pick coin A then coin B, or coin B then coin A, we might end up needing the same remaining amount.
Pattern 3: Grid problems with multiple paths
In a grid, paths from top-left to bottom-right can reach the same cell via different routes. Each route triggers the same subproblem from that cell.
12345678910111213141516171819
WHY fib(n-1) AND fib(n-2) CAUSE OVERLAP:━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ fib(n) / \ fib(n-1) fib(n-2) / \ | fib(n-2) fib(n-3)| ↓ ↓ CONVERGENCE POINT! fib(n-1) calls fib(n-2) as one of its children.fib(n-2) IS its sibling.Same subproblem, two paths! This continues at every level:• fib(n-2) appears in fib(n-1)'s subtree AND as fib(n)'s right child• fib(n-3) appears in fib(n-2)'s subtree AND in fib(n-1)'s subtree• The overlap compounds exponentially1234567891011121314151617181920
Problem: Count paths from (0,0) to (m,n) moving only right or down. (0,0) / \ (0,1) (1,0) / \ / \ (0,2) (1,1) (1,1) (2,0) ↑ ↑ SAME CELL! Multiple paths to (1,1):• Right then Down• Down then Right Once at (1,1), the remaining subproblem is identical:"count paths from (1,1) to (m,n)" Without memoization, we solve this subproblem twice.For a large grid, the same cells are reached via MANY paths,causing exponential redundancy.Redundancy happens when THE CHOICES MADE TO REACH A SUBPROBLEM DON'T AFFECT THE SUBPROBLEM'S ANSWER. In Fibonacci, whether we arrived at fib(3) via fib(4) or fib(5), the answer is still 2. This path-independence is the key signal for overlapping subproblems.
Once we identify redundancy, the solution is conceptually simple: remember answers to subproblems we've already solved. This technique is called memoization (not "memorization"—it comes from "memo," as in writing notes).
The memoization pattern:
Effect on the recursion tree:
Memoization effectively prunes the tree. When we encounter a repeated subproblem, we don't expand its subtree—we just use the cached answer. The expanded portions of the tree match the DAG of unique subproblems.
1234567891011121314151617181920212223242526
# Naive Fibonacci: O(2^n) timedef fib_naive(n): if n <= 1: return n return fib_naive(n - 1) + fib_naive(n - 2) # Memoized Fibonacci: O(n) timedef fib_memo(n, memo={}): if n in memo: # Check if already solved return memo[n] # Return cached answer if n <= 1: return n # Compute, store, and return memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo) return memo[n] # Alternative: Using @lru_cache (Python)from functools import lru_cache @lru_cache(maxsize=None)def fib_cached(n): if n <= 1: return n return fib_cached(n - 1) + fib_cached(n - 2)1234567891011121314151617181920212223242526272829303132
WITHOUT MEMOIZATION (full tree expansion): fib(5) / \ fib(4) fib(3) / \ / \ fib(3) fib(2) fib(2) fib(1) / \ / \ / \ fib(2) fib(1)... ... ... / \fib(1) fib(0) Total nodes: 15 (exponential in n) WITH MEMOIZATION (pruned after first encounter): fib(5) / \ fib(4) fib(3) → [CACHED] / \ fib(3) fib(2) → [CACHED] / \ fib(2) fib(1) / \ ↓fib(1) fib(0) [base case] ↓ ↓[base] [base] Expanded nodes: 9 (but only 6 unique computations)Time complexity: O(n) The memo table after fib(5):{0: 0, 1: 1, 2: 1, 3: 2, 4: 3, 5: 5}Memoization transforms the recursion tree into the underlying DAG traversal. Each edge in the DAG is traversed once, each node is computed once. The exponential blowup was caused by traversing the same DAG paths repeatedly—memoization ensures each path is traversed exactly once.
Memoization is "top-down" dynamic programming—we start with the original problem and recursively break it down, caching as we go. Bottom-up dynamic programming achieves the same result by building solutions from the smallest subproblems up.
The key insight:
If we know the DAG structure (i.e., which subproblems depend on which), we can solve subproblems in an order that ensures dependencies are solved first. No recursion needed—just iteration.
123456789101112
def fib_topdown(n, memo={}): if n in memo: return memo[n] if n <= 1: return n memo[n] = fib_topdown(n-1, memo) + \ fib_topdown(n-2, memo) return memo[n] # Starts at n, recurses down to 0,1# Fills memo as recursion returns# Order: unpredictable, demand-driven123456789101112
def fib_bottomup(n): if n <= 1: return n dp = [0] * (n + 1) dp[0], dp[1] = 0, 1 for i in range(2, n + 1): dp[i] = dp[i-1] + dp[i-2] return dp[n] # Starts at 0, 1, iterates up to n# Fills table in predictable order# No recursion overheadBoth approaches:
Differences:
| Aspect | Top-Down (Memoization) | Bottom-Up (Tabulation) |
|---|---|---|
| Implementation | Recursive + cache | Iterative + table |
| Order of solving | Demand-driven, unpredictable | Predetermined, in order |
| Subproblems solved | Only those needed | All subproblems up to n |
| Stack space | O(n) recursion stack | O(1) — no recursion |
| Ease of writing | Often more intuitive | Requires topological ordering |
| Space optimization | Harder to optimize | Often easy to reduce space |
Understanding the recursion tree helps with both approaches. For top-down, the tree shows which subproblems to cache. For bottom-up, the DAG (collapsed tree) reveals the computation order: we must solve subproblems before the problems that depend on them.
Not every recursive algorithm benefits from memoization. Here's how to quickly assess whether memoization will help:
Step 1: Check for repeated subproblems
Draw a few levels of the recursion tree. Do you see duplicate labels? If every node is unique (like merge sort or binary search), memoization won't help.
Step 2: Count unique vs total subproblems
What are the possible parameter combinations? For fib(n), parameters range from 0 to n—that's n+1 unique possibilities. For a grid problem on an m×n grid, you have at most m×n unique positions.
Step 3: Compare with tree size
If unique subproblems are polynomial but tree size is exponential, memoization offers big gains. If they're the same order, no benefit.
| Problem | Unique Subproblems | Tree Size (Naive) | Memoization Benefit |
|---|---|---|---|
| Fibonacci | O(n) | O(2ⁿ) | HUGE: exponential → linear |
| Coin Change | O(n × amount) | O(coins^amount) | HUGE: exponential → polynomial |
| Grid Paths | O(m × n) | O(2^(m+n)) | HUGE: exponential → polynomial |
| LCS | O(m × n) | O(2^(m+n)) | HUGE: exponential → polynomial |
| Merge Sort | O(n) | O(n) | NONE: already optimal |
| Binary Search | O(log n) | O(log n) | NONE: already optimal |
| Factorial | O(n) | O(n) | NONE: already optimal |
The quickest test: How many distinct parameter combinations exist? That's your upper bound on unique subproblems. If this number is polynomial and your naive recursion is exponential, memoization will provide dramatic speedup. For fib(n), parameters are a single integer 0..n, so n+1 unique subproblems—clearly polynomial.
Let's examine redundancy patterns in other classic problems to strengthen pattern recognition.
12345678910111213141516171819
Problem: How many ways to climb n stairs taking 1 or 2 steps at a time? def climbStairs(n): if n <= 2: return n return climbStairs(n-1) + climbStairs(n-2) # Same as Fibonacci! Recursion tree for climbStairs(5): climb(5) / \ climb(4) climb(3) ←── REPEATED / \ / \ climb(3) climb(2) climb(2) climb(1) ↑ ↑ ↑ REPEATED REPEATED REPEATED Unique subproblems: nNaive tree size: O(2ⁿ)With memoization: O(n)1234567891011121314151617181920212223242526
Problem: Minimum coins to make amount using coins = [1, 2, 5] def minCoins(coins, amount): if amount == 0: return 0 if amount < 0: return infinity return 1 + min(minCoins(coins, amount - c) for c in coins) Recursion tree for minCoins([1,2,5], 6): minCoins(6) / | \ minCoins(5) minCoins(4) minCoins(1) / | \ / | \ | (4) (3) (0) (3) (2) (-1) (0) ↑ ↑ SAME AS ALSO APPEARS minCoins(4) ELSEWHERE Subproblem minCoins(k) is reached via: 6-1-...-1 = k 6-2-...-2 = k 6-5-1 = k ... many paths to the same k! Unique subproblems: O(amount) = O(n)Naive tree: exponential (up to O(coins^amount))With memoization: O(coins × amount)1234567891011121314151617181920212223
Problem: Find LCS of strings X[0..m] and Y[0..n] def lcs(X, Y, i, j): if i == 0 or j == 0: return 0 if X[i-1] == Y[j-1]: return 1 + lcs(X, Y, i-1, j-1) return max(lcs(X, Y, i-1, j), lcs(X, Y, i, j-1)) Recursion tree for lcs("AB", "AC", 2, 2): lcs(2,2) / \ lcs(1,2) lcs(2,1) / \ / \ lcs(0,2) lcs(1,1) lcs(1,1) lcs(2,0) ↑ ↑ SAME SUBPROBLEM! lcs(1,1) is computed twice—once from (1,2), once from (2,1).As strings get longer, overlap increases dramatically. Unique subproblems: O(m × n) — all (i,j) pairsNaive tree: O(2^(m+n)) — each call branches two waysWith memoization: O(m × n) time and spaceIn all these examples, redundancy arises from the same mechanism: different sequences of choices lead to the same subproblem. Memoization works because the ANSWER to a subproblem doesn't depend on HOW we got there—only on the current state (parameter values).
You now understand how recursion trees reveal redundancy and how to exploit this for optimization. Let's consolidate the key insights:
What's next:
You've completed the module on visualizing recursion trees. You now have a powerful set of tools: you can draw trees from code, analyze complexity from tree structure, and identify optimization opportunities through redundancy detection. These skills form the foundation for the upcoming chapters on dynamic programming and advanced recursive techniques.
Congratulations! You've mastered recursion tree visualization. You can now draw recursion trees, analyze time and space complexity from their structure, and identify the overlapping subproblems that lead to devastating redundancy. This foundation prepares you for dynamic programming, where you'll learn to systematically eliminate redundancy and transform exponential algorithms into polynomial ones.