You're standing at the bottom of a staircase. You can take 1 or 2 steps at a time. How many ways can you reach the top? If you try to brute-force this, you end up computing the same sub-problems hundreds of times. Dynamic Programming says: solve each sub-problem once, remember the answer, and reuse it. That single idea is worth a thousand interviews.

What is Dynamic Programming?

Dynamic Programming (DP) is an algorithmic technique for solving problems by breaking them into overlapping sub-problems, solving each sub-problem only once, and storing the result so it can be reused. It is most applicable when a problem has two properties:

DP is not about a specific data structure or algorithm — it's a problem-solving paradigm. The two implementations (memoization and tabulation) are different ways of applying the same idea.

Two Ways to Implement DP

Top-Down (Memoization)

Start from the original problem and recurse down. Cache each result so you never solve the same sub-problem twice. Natural to write — just add a cache to your recursion.

Bottom-Up (Tabulation)

Start from the smallest sub-problems and build up to the answer iteratively. Uses a table (usually an array). No recursion, no stack overflow risk, often faster in practice.

Both approaches give the same answer. Choose memoization when the recursion is natural and not all sub-problems are needed. Choose tabulation when you want a clean iterative solution with O(1) space optimization potential.

Warm-Up: Fibonacci Numbers

The classic entry point into DP. The naive recursion is O(2ⁿ). DP drops it to O(n).

Naive Recursion — O(2ⁿ)

python — Naive Fibonacci
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

# fib(6) recomputes fib(2) five times, fib(3) three times…
# For n = 40: ~2 billion calls!

Top-Down DP (Memoization) — O(n) time, O(n) space

python — Memoized Fibonacci
from functools import lru_cache

@lru_cache(maxsize=None)
def fib_memo(n):
    if n <= 1:
        return n
    return fib_memo(n - 1) + fib_memo(n - 2)

# Each value computed exactly once, stored in cache.
# fib_memo(40) → instant.

Bottom-Up DP (Tabulation) — O(n) time, O(n) space

python — Tabulated Fibonacci
def fib_tab(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]

# dp table for n = 6:
# Index: 0  1  2  3  4  5  6
# Value: 0  1  1  2  3  5  8

Space-Optimised — O(n) time, O(1) space

python — Space-Optimised Fibonacci
def fib_opt(n):
    if n <= 1:
        return n
    a, b = 0, 1
    for _ in range(2, n + 1):
        a, b = b, a + b
    return b

# Only two variables needed — no full table required.

Always ask: do I need the entire table, or just the last few values? Many DP problems can be space-optimised from O(n) or O(n²) down to O(1) or O(n) once you notice only a sliding window of the table is ever accessed.

Problem 1: Climbing Stairs

You can climb 1 or 2 steps at a time. Given n stairs, how many distinct ways can you reach the top?

Recurrence: ways(n) = ways(n-1) + ways(n-2) — identical structure to Fibonacci.

python — Climbing Stairs Solution
def climb_stairs(n):
    if n <= 2:
        return n
    a, b = 1, 2
    for _ in range(3, n + 1):
        a, b = b, a + b
    return b

# climb_stairs(5) → 8
n (stairs)123456
ways(n)1235813

Problem 2: Coin Change (Minimum Coins)

Given coin denominations and a target amount, find the minimum number of coins needed to make that amount. If it's impossible, return -1.

Example: coins = [1, 3, 4], amount = 6 → answer is 2 (3 + 3).

Recurrence: dp[i] = min(dp[i - c] + 1) for each coin c where c ≤ i.

python — Coin Change Algorithm
def coin_change(coins, amount):
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0

    for i in range(1, amount + 1):
        for coin in coins:
            if coin <= i:
                dp[i] = min(dp[i], dp[i - coin] + 1)

    return dp[amount] if dp[amount] != float('inf') else -1

print(coin_change([1, 3, 4], 6))    # → 2  (3+3)
print(coin_change([2], 3))           # → -1 (impossible)
print(coin_change([1, 5, 6, 9], 11)) # → 2  (5+6)
Amount0123456
dp[i]0121122
Howbase11+1344+13+3

Problem 3: 0/1 Knapsack

Given n items, each with a weight and value, and a bag with capacity W — find the maximum value you can carry. Each item can be taken at most once.

Recurrence:

python — 0/1 Knapsack DP
def knapsack(weights, values, capacity):
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]

    for i in range(1, n + 1):
        for w in range(capacity + 1):
            dp[i][w] = dp[i - 1][w]
            if weights[i - 1] <= w:
                take = dp[i - 1][w - weights[i - 1]] + values[i - 1]
                dp[i][w] = max(dp[i][w], take)

    return dp[n][capacity]

weights = [1, 3, 4, 5]
values  = [1, 4, 5, 7]
capacity = 7
print(knapsack(weights, values, capacity))  # → 9
Item \ Cap01234567
0 (none)00000000
1 (w=1,v=1)01111111
2 (w=3,v=4)01145555
3 (w=4,v=5)01145669
4 (w=5,v=7)01145789

The naive brute-force for Knapsack checks all 2ⁿ subsets — that's 1 billion for n=30. DP solves it in O(n × W) time and O(n × W) space, which for n=30, W=1000 is just 30,000 operations.

Problem 4: Longest Common Subsequence (LCS)

Given two strings, find the length of their longest common subsequence — characters that appear in the same relative order in both strings (not necessarily contiguous).

Example: s1 = "ABCBDAB", s2 = "BDCAB" → LCS = "BCAB" (length 4).

Recurrence:

python — Longest Common Subsequence
def lcs(s1, s2):
    m, n = len(s1), len(s2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]

    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if s1[i - 1] == s2[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

    return dp[m][n]

print(lcs("ABCBDAB", "BDCAB"))   # → 4
print(lcs("AGGTAB", "GXTXAYB"))  # → 4
print(lcs("abcde", "ace"))        # → 3
""ACBD
""00000
A01111
B01122
C01222
D01223

Problem 5: Longest Increasing Subsequence (LIS)

Find the length of the longest subsequence of an array such that all elements are in strictly increasing order.

Example: [10, 9, 2, 5, 3, 7, 101, 18] → LIS = [2, 3, 7, 18] → length 4.

python — Longest Increasing Subsequence
def lis(arr):
    n = len(arr)
    dp = [1] * n

    for i in range(1, n):
        for j in range(i):
            if arr[j] < arr[i]:
                dp[i] = max(dp[i], dp[j] + 1)

    return max(dp)

print(lis([10, 9, 2, 5, 3, 7, 101, 18]))  # → 4
print(lis([0, 1, 0, 3, 2, 3]))             # → 4
print(lis([7, 7, 7, 7]))                   # → 1
Index01234567
arr[i]109253710118
dp[i]11122344

Problem 6: Matrix Chain Multiplication

Given a chain of matrices, find the most efficient order of multiplication to minimise the total number of scalar multiplications.

Recurrence: dp[i][j] = min over all k of (dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j])

python — Matrix Chain Multiplication
def matrix_chain(p):
    n = len(p) - 1
    dp = [[0] * (n + 1) for _ in range(n + 1)]

    for length in range(2, n + 1):
        for i in range(1, n - length + 2):
            j = i + length - 1
            dp[i][j] = float('inf')
            for k in range(i, j):
                cost = dp[i][k] + dp[k+1][j] + p[i-1] * p[k] * p[j]
                dp[i][j] = min(dp[i][j], cost)

    return dp[1][n]

p = [40, 20, 30, 10, 30]
print(matrix_chain(p))  # → 26000

Brute force tries all possible parenthesisations — that's a Catalan number, which grows exponentially. DP solves it in O(n³) time and O(n²) space.

How to Approach Any DP Problem

1
Identify the problem type

Does it ask for a maximum/minimum value, count of ways, or true/false feasibility? These are classic DP signals.

2
Define the state

What does dp[i] or dp[i][j] represent? The definition of your state is the core of your solution.

3
Write the recurrence

Express dp[i] in terms of smaller sub-problems. Think about what choices are available at each step.

4
Identify base cases

What are the smallest sub-problems with known answers? These seed the rest of the table.

5
Choose top-down or bottom-up

Memoization is easier to write first. Convert to tabulation if you need better space or want to avoid recursion depth limits.

6
Optimise space if needed

Check whether you only access the previous row/column/element. If so, reduce the table to a 1D array or a few variables.

Common DP Patterns to Recognise

Linear DP

State depends on previous elements in a 1D array. Examples: Fibonacci, Climbing Stairs, LIS, Max Subarray Sum.

2D DP / Grid DP

State indexed by two variables. Examples: Knapsack, LCS, Edit Distance.

Interval DP

Fill a table diagonally over all sub-intervals. Examples: Matrix Chain Multiplication, Burst Balloons.

Tree DP

DP on a tree structure, often computing values bottom-up from leaves. Examples: Max Path Sum, House Robber III.

Bitmask DP

State includes a bitmask to track which items/nodes are visited. Examples: Travelling Salesman Problem.

Probability / Count DP

Count the number of ways or compute probabilities. Examples: Coin Change (ways), Unique Paths.

Complexity Summary

ProblemTimeSpaceSpace-Optimised
FibonacciO(n)O(n)O(1)
Climbing StairsO(n)O(n)O(1)
Coin ChangeO(n × amount)O(amount)O(amount)
0/1 KnapsackO(n × W)O(n × W)O(W)
LCSO(m × n)O(m × n)O(min(m,n))
LISO(n²)O(n)O(n log n) with BSearch
Matrix ChainO(n³)O(n²)O(n²)

Practice challenge: Given coins = [1, 2, 5] and amount = 5, find the total number of ways to make that amount. Hint: it's a variation of Coin Change — what changes in the recurrence? (Answer: dp[i] += dp[i - coin] instead of min. For this example: 4 ways.)

Dynamic Programming is one of those topics that clicks suddenly — and once it does, you start seeing DP everywhere. Start with Fibonacci, then Climbing Stairs, then Coin Change. Work through Knapsack and LCS with pen and paper, filling the table manually. That muscle memory of "define state → write recurrence → fill table" is all you need to crack any DP problem, in any interview, at any company.