Skip to content
DSA Patterns

Learn

Dynamic Programming

A practical route into DP: find the state, write the recurrence, memoise, then tabulate — plus the five families (knapsack, LIS, two-sequence, interval, grid) that cover most questions.

4 min readUpdated 2 Sept 2026

#Dynamic Programming#Recursion#Memoization#Optimization

DP is the section people avoid, and the avoidance is a recognition problem rather than a maths one. Every DP solution is a recursion whose subproblems repeat; the technique is noticing the repetition and paying for each subproblem once. Learn the families and most questions become "which one is this".

The four steps

Do these in order, every time. The mistake is trying to write the table first.

  1. State — what arguments distinguish one subproblem from another? dp[i] means

what, exactly, in one sentence?

  1. Recurrence — how does a state's answer follow from smaller ones?
  2. Base cases — the smallest states, answered directly.
  3. Order — memoised recursion (top-down) or a filled table (bottom-up).

Write the brute-force recursion first, then add a cache. That mechanical route gets you to a working solution under pressure far more reliably than trying to see the table.

from functools import cache

def coin_change(coins, amount):
    @cache
    def fewest(remaining):
        if remaining == 0:
            return 0
        if remaining < 0:
            return float("inf")
        return min((1 + fewest(remaining - c) for c in coins), default=float("inf"))

    result = fewest(amount)
    return -1 if result == float("inf") else result

That is the whole method: an obvious recursion, plus one decorator. Converting to a bottom-up table afterwards is a mechanical rewrite and often unnecessary.

The one-dimensional family

dp[i] depends on a constant number of previous entries.

def rob(nums):
    prev = curr = 0
    for x in nums:
        prev, curr = curr, max(curr, prev + x)   # skip this house, or take it
    return curr

Climbing Stairs, House Robber, Min Cost Climbing Stairs, Decode Ways and Fibonacci are all this shape. Because only the last two states matter, the array collapses to two variables — the standard space optimisation, and the follow-up you will be asked for.

House Robber II (a circle) is the neat one: run the linear solution twice, once excluding

the first house and once excluding the last, and take the better. Recognising that a constraint can be removed by solving two easier instances is a transferable idea.

The knapsack family

Choose a subset subject to a capacity. State is dp[i][capacity].

  • 0/1 — each item used at most once. Iterate capacity downward in the 1-D version.
  • Unbounded — items reusable. Iterate capacity upward.
def can_partition(nums):
    total = sum(nums)
    if total % 2:
        return False
    target = total // 2
    reachable = [False] * (target + 1)
    reachable[0] = True

    for x in nums:
        for s in range(target, x - 1, -1):        # DOWNWARD: each item once
            reachable[s] |= reachable[s - x]
    return reachable[target]

That loop direction is the entire difference between the two variants, and reversing it by accident is the most common DP bug there is. Downward means reachable[s - x] still refers to the state before this item was considered.

Coin Change (unbounded), Coin Change II (counting), Target Sum and Partition Equal Subset Sum are all knapsacks in disguise.

The two-sequence family

Comparing two strings: dp[i][j] covers the first i of one and the first j of the other. Match the characters, or don't.

def longest_common_subsequence(a, b):
    dp = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
    for i in range(1, len(a) + 1):
        for j in range(1, len(b) + 1):
            if a[i - 1] == b[j - 1]:
                dp[i][j] = 1 + dp[i - 1][j - 1]       # consume both
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
    return dp[-1][-1]
BDCBABCB0000000000011110112201123the three predecessorsdp[i-1][j-1]dp[i-1][j]dp[i][j-1]answer = 3
Characters match → take the diagonal and add one. They do not → take the better of above and left. Row and column zero mean “empty prefix”, which removes every boundary case.

Edit Distance is the same grid with three predecessors instead of two — insert, delete, replace — and Distinct Subsequences, Regular Expression Matching and Wildcard Matching all live on this grid too. The +1 offsets are so that row and column 0 mean "empty prefix", which removes every boundary special case.

The LIS family

dp[i] = the best answer ending at i. Quadratic by default:

def length_of_lis(nums):
    dp = [1] * len(nums)
    for i in range(len(nums)):
        for j in range(i):
            if nums[j] < nums[i]:
                dp[i] = max(dp[i], dp[j] + 1)
    return max(dp, default=0)

There is an O(n log n) version using patience sorting and binary search over a "tails" array — worth knowing, because O(n²) invites the follow-up. Maximum Subarray (Kadane's) is the degenerate case where only the immediately preceding state matters.

The interval family

dp[i][j] covers the range i..j, and you iterate by increasing length. Burst Balloons, Matrix Chain Multiplication and Longest Palindromic Substring are here. The reframe that makes Burst Balloons work — think about which balloon is burst last in a range, not first — is the single hardest idea on the sheet, and it is the reason interval DP is worth a separate look.

Complexity

States × work per state. 1-D families are O(n); knapsack is O(n × capacity); two-sequence is O(m × n); interval is O(n³). Space usually reduces by one dimension, because each row depends only on the previous one — say that unprompted.

Mistakes that cost the round

  • Writing the table before the recurrence. State first, always.
  • Wrong loop direction in 1-D knapsack, silently turning 0/1 into unbounded.
  • Sloppy base cases, especially the empty-string row and column.
  • Memoising on an incomplete state. If the answer depends on something not in the cache

key, the cache returns wrong answers.

  • Optimising space before it is correct. Get the table right, then collapse it.

What to drill

In this order — each introduces exactly one new idea:

  1. Climbing StairsHouse Robber — 1-D states.
  2. Coin ChangeCoin Change II — unbounded knapsack, min then count.
  3. Partition Equal Subset Sum — 0/1 knapsack and the loop direction.
  4. Longest Common SubsequenceEdit Distance — the two-sequence grid.
  5. Longest Increasing Subsequence — ending-at-i states.
  6. Word Break, Unique Paths, Jump Game — the common variants.
  7. Burst Balloons, Regular Expression Matching — interval and hard grid DP.

All 35 are on the 22 DSA Patterns sheet.

Frequently asked

How do I know a problem is dynamic programming?

Two signs together: the problem asks for an optimum or a count over a sequence of choices, and a brute-force recursion would solve the same subproblem repeatedly. If choices are independent and a locally best pick is provably safe, it is greedy instead; if subproblems overlap and a local choice can be wrong, it is DP.

Should I write top-down memoization or bottom-up tabulation?

Top-down first. It is the brute-force recursion plus a cache, so it is far quicker to get right under pressure and it only visits reachable states. Convert to bottom-up when you need the space optimisation or want to avoid recursion depth limits — the rewrite is mechanical once the recurrence is correct.

Why does the loop direction matter in knapsack?

In the space-optimised 1-D version, iterating capacity downward means dp[s - x] still holds the value from before the current item was considered, so each item is used at most once — 0/1 knapsack. Iterating upward lets the item's own update feed back into itself, which is exactly the unbounded variant. One reversed loop silently changes which problem you solved.

Related