Recognising DP: Overlapping Sub-Problems
Identify when brute-force recursion re-solves the same sub-problem, draw the recursion tree for Fibonacci, and see the exponential blowup.
Recognising DP: Overlapping Sub-Problems is a free DSA Interview Prep lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the DSA Interview Prep learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What is Dynamic Programming?
Dynamic programming (DP) solves complex problems by breaking them into simpler overlapping sub-problems, solving each sub-problem once, and storing the result to avoid redundant computation. DP applies when a problem has two ingredients: overlapping sub-problems (same sub-problem is solved multiple times in a naive recursion) and optimal substructure (the optimal solution can be built from optimal solutions to sub-problems). Without both ingredients, DP does not help.
# Two ingredients of DP:
# 1. Overlapping sub-problems:
# fib(5) -> fib(4) + fib(3)
# fib(4) -> fib(3) + fib(2) <- fib(3) computed twice!
# Without caching: O(2^n) calls for Fibonacci
# 2. Optimal substructure:
# Shortest path from A to C through B:
# shortest(A,C) = shortest(A,B) + shortest(B,C)
# The sub-path A->B must itself be the shortest
# Contrast with greedy: greedy makes one locally optimal
# choice; DP tries all choices and picks the best.
print('DP = overlapping sub-problems + optimal substructure')Fibonacci: The Classic DP Entry Point
The Fibonacci sequence (fib(n) = fib(n-1) + fib(n-2)) is the canonical example of overlapping sub-problems. The naive recursion has exponential time O(2^n) because it recomputes the same values repeatedly. The recursion tree for fib(6) shows fib(3) computed 3 times, fib(2) 5 times, and so on. This exponential blowup is exactly what DP eliminates by storing computed results.
import time
def fib_naive(n):
if n <= 1:
return n
return fib_naive(n-1) + fib_naive(n-2)
# Count the calls:
call_count = [0]
def fib_count(n):
call_count[0] += 1
if n <= 1: return n
return fib_count(n-1) + fib_count(n-2)
fib_count(10)
print(f'Calls for fib(10): {call_count[0]}') # 177 calls for n=10!
call_count[0] = 0
fib_count(20)
print(f'Calls for fib(20): {call_count[0]}') # 21891 calls
# n=30 -> ~2.7 million calls: exponential growthVisualising the Recursion Tree
Drawing the recursion tree for fib(5) reveals the waste: each node spawns two children, and identical sub-trees appear repeatedly. The total number of nodes in the tree is O(2^n). When you see this pattern — identical function calls with the same arguments repeated in the tree — it signals that DP can help by caching results. This visualisation skill is crucial: if you can identify the repeated sub-trees, you know DP is applicable.
# fib(5) recursion tree (simplified):
# fib(5)
# / \
# fib(4) fib(3)
# / \ / \
# fib(3) fib(2) fib(2) fib(1)
# / \ \
# fib(2) fib(1) fib(1)
# / \
# fib(1) fib(0)
# fib(3) appears TWICE
# fib(2) appears THREE TIMES
# Each redundant call wastes exponential time
# Key insight: fib(n) only has O(n) DISTINCT sub-problems
# (fib(0), fib(1), ..., fib(n))
# DP computes each ONCE -> O(n) total
print('Distinct sub-problems: O(n) but naive calls: O(2^n)')Identifying Overlapping Sub-Problems
To recognise overlapping sub-problems: write the brute-force recursion, then ask 'are there multiple recursive calls with the SAME arguments?' If yes, DP can help. Common signals in problem descriptions: 'minimum/maximum number of X', 'how many ways to Y', 'can we achieve Z?'. These phrasing patterns almost always indicate an optimal substructure problem where the answer at position i depends on answers at earlier positions.
# DP signal phrases in problem statements:
# 'minimum number of coins to make amount X'
# 'maximum profit from stock trades'
# 'number of ways to climb n stairs'
# 'can you reach the last index?'
# 'longest common subsequence'
# 'edit distance between two strings'
# All have this shape:
# solve(input) = f(solve(smaller_input_1), solve(smaller_input_2), ...)
# And multiple branches end up calling solve with the same argument.
# If the recursion tree has repeated nodes: DP
# If subproblems are all independent: divide-and-conquer (no DP needed)
print('Repeated arguments in recursion tree -> DP')Optimal Substructure Explained
Optimal substructure means the optimal solution to the problem can be constructed from optimal solutions to its sub-problems. For example, the shortest path from A to C through B is optimal iff the sub-paths A→B and B→C are each individually optimal. If this property holds, you can build the global optimum bottom-up from local optima. Problems that lack optimal substructure (e.g., longest path in a general graph with cycles) cannot be solved with DP.
# Optimal substructure examples:
# SHORTEST PATH: shortest(A,C) = min over all B: shortest(A,B) + w(B,C)
# -> Sub-paths must be optimal: YES, has optimal substructure
# LONGEST PATH (no cycles, DAG): can also use DP
# -> Longer path through node B means sub-path A->B must be longest
# LONGEST PATH (with cycles): NO optimal substructure
# -> Best path from A to C might reuse nodes: sub-problems not independent
# COIN CHANGE: min coins for amount n = 1 + min(min coins for n-coin_i)
# -> YES: optimal for n-coin_i is needed for optimal n
print('Optimal substructure: build global optimum from local optima')Climbing Stairs: Your First DP
Climbing Stairs (LeetCode #70): how many distinct ways can you climb n stairs taking 1 or 2 steps at a time? Let dp[i] = number of ways to reach stair i. You can arrive at stair i from stair i-1 (one step) or stair i-2 (two steps), so dp[i] = dp[i-1] + dp[i-2]. This is Fibonacci! Base cases: dp[1] = 1, dp[2] = 2. Recognising that 'climbing stairs' reduces to Fibonacci is a classic interview insight.
def climb_stairs(n):
if n <= 2:
return n
dp = [0] * (n + 1)
dp[1] = 1 # 1 way to reach step 1
dp[2] = 2 # 2 ways to reach step 2: (1+1) or (2)
for i in range(3, n + 1):
dp[i] = dp[i-1] + dp[i-2] # come from i-1 or i-2
return dp[n]
for n in range(1, 8):
print(f'climb_stairs({n}) = {climb_stairs(n)}')
# 1, 2, 3, 5, 8, 13, 21 -- Fibonacci sequence!The DP Framework: Define, Recur, Order
A reliable 3-step DP framework: 1. Define the state — what does dp[i] (or dp[i][j]) represent? Write it in English. 2. Write the recurrence — express dp[i] in terms of smaller sub-problems. Include all cases. 3. Determine the fill order — ensure dp[i-1] (and other dependencies) are computed before dp[i]. Base cases initialise the boundary. This framework converts fuzzy DP intuition into a concrete implementation plan.
# Framework applied to climbing stairs:
# Step 1 - Define state:
# dp[i] = number of distinct ways to reach step i
# Step 2 - Recurrence:
# dp[i] = dp[i-1] + dp[i-2] (come from step i-1 or i-2)
# Step 3 - Fill order:
# Compute dp[1], dp[2], dp[3], ..., dp[n] in order
# Because dp[i] depends on dp[i-1] and dp[i-2] (smaller)
# Base cases: dp[1]=1, dp[2]=2
# Framework applied to coin change:
# Step 1: dp[amount] = minimum coins to make that amount
# Step 2: dp[i] = 1 + min(dp[i-coin] for coin in coins if i >= coin)
# Step 3: Fill i from 1 to amount
# Base: dp[0] = 0 (zero coins for zero amount)
print('DP framework: define state -> recurrence -> fill order')When NOT to Use DP
DP is not always the answer. Use greedy when a single locally optimal choice always leads to the globally optimal solution (activity selection, jump game I). Use divide and conquer when sub-problems do not overlap (merge sort, binary search). Use BFS when the problem is shortest path in an unweighted graph. DP is correct but often overkill when a greedy or simpler approach exists. In interviews, discuss why you chose DP over alternatives.
# DP vs alternatives:
# Problem: can you jump to the end of the array?
# Greedy: track max reachable index -> O(n) O(1) BETTER than DP
# Problem: shortest path unweighted graph?
# BFS: O(V+E) BETTER than DP on general graph
# Problem: sort an array?
# Comparison sort: O(n log n), no DP needed
# DP IS the right choice when:
# - Greedy fails (choices interact)
# - Need to count/enumerate all possibilities
# - Problem has 'how many ways' or 'minimum/maximum' flavor
# - Recursion tree clearly shows overlapping sub-problems
print('Ask: does greedy fail? If yes, consider DP.')Counting Distinct Sub-Problems
The number of distinct sub-problems determines the DP's time and space complexity. For a 1D DP on input of size n, there are O(n) sub-problems. For a 2D DP on two inputs of sizes m and n, there are O(mn) sub-problems. Each sub-problem is solved in O(k) time (for k choices at each step), giving total time O(n*k) or O(mn*k). Always count distinct sub-problems first — this gives the DP's time complexity before you even code it.
# Sub-problem count examples:
# Problem | Sub-problems | Each costs | Total
# Fibonacci | O(n) | O(1) | O(n)
# Coin change | O(amount) | O(coins) | O(amount * coins)
# LCS (m,n chars) | O(m*n) | O(1) | O(m*n)
# Edit distance | O(m*n) | O(1) | O(m*n)
# 0/1 Knapsack | O(n*W) | O(1) | O(n*W)
# Matrix chain | O(n^2) | O(n) | O(n^3)
# Rule: DP time = (# distinct sub-problems) * (time per sub-problem)
print('Time = subproblems * work-per-subproblem')House Robber: Overlapping Choices
House Robber (LeetCode #198) asks for the maximum amount you can rob from houses in a row without robbing adjacent houses. At each house, you choose: rob it (add its value, skip the previous) or skip it (take the best from the previous). dp[i] = max(dp[i-1], dp[i-2] + nums[i]). This choice-at-each-step pattern is the simplest 1D DP recurrence and appears in dozens of interview problems.
def rob(nums):
if not nums: return 0
if len(nums) == 1: return nums[0]
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i-1], # skip house i
dp[i-2] + nums[i]) # rob house i
return dp[-1]
print(rob([1, 2, 3, 1])) # 4: rob house 0 and 2 (1+3)
print(rob([2, 7, 9, 3, 1]))# 12: rob house 0, 2, 4 (2+9+1)
print(rob([2, 1, 1, 2])) # 4: rob house 0 and 3Sanity Check: Brute-Force vs DP
Always verify your DP against a brute-force solution on small inputs. The brute-force is your ground truth. Once the DP matches the brute-force on all test cases, you know the recurrence is correct. Only then optimise for space. This test-driven approach — brute-force → top-down DP → bottom-up DP → space-optimised DP — is the professional way to develop and verify DP solutions during an interview.
# Brute-force for house robber (exponential)
def rob_brute(nums, i=0):
if i >= len(nums):
return 0
# Option 1: rob house i
rob_it = nums[i] + rob_brute(nums, i + 2)
# Option 2: skip house i
skip_it = rob_brute(nums, i + 1)
return max(rob_it, skip_it)
# Verify on small inputs:
test_cases = [[1,2,3,1], [2,7,9,3,1], [2,1,1,2]]
for tc in test_cases:
bf = rob_brute(tc)
dp = rob(tc)
print(f'{tc}: brute={bf}, dp={dp}, match={bf==dp}')Quick Check
Test your understanding of Data Structures & Algorithms — Coding Interview Prep concepts from this lesson.
Lesson Recap
In this lesson you learned: two DP ingredients (overlapping sub-problems and optimal substructure), how to visualise the recursion tree to identify repeated calls, the three-step DP framework (define state, recurrence, fill order), and first examples including Fibonacci, climbing stairs, and house robber. Next up we implement top-down DP with memoisation.
Frequently asked questions
Is the “Recognising DP: Overlapping Sub-Problems” lesson free?
Yes — the full text of “Recognising DP: Overlapping Sub-Problems” is free to read here on the web, and the DSA Interview Prep course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the DSA Interview Prep course, upgrade to CoddyKit PRO.
What will I learn in “Recognising DP: Overlapping Sub-Problems”?
Identify when brute-force recursion re-solves the same sub-problem, draw the recursion tree for Fibonacci, and see the exponential blowup. You practise DSA Interview Prep with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start DSA Interview Prep?
No prior experience is required. DSA Interview Prep on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Recognising DP: Overlapping Sub-Problems” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this DSA Interview Prep lesson?
Yes. Every DSA Interview Prep lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Recognising DP: Overlapping Sub-Problems
- Top-Down DP with Memoisation
- Bottom-Up DP with Tabulation
- Coin Change and Min-Cost Staircase