Skip to content

Dynamic Programming Patterns

Recognising which DP pattern applies to a problem is the key skill. This section provides a decision Framework.

SignalPatternExample
Choose items with capacity constraintKnapsack0/1 knapsack, subset sum
Optimise over intervals/substringsInterval DPMatrix chain, burst balloons
Problem on a tree structureTree DPDiameter, independent set
Small set of items (n <= 20)Bitmask DPTSP, assignment
Count numbers with digit propertiesDigit DPNumbers with no “4’ and ‘7’
Optimise over subsequences/stringsString DPEdit distance, LCS
Two players taking turnsGame theory DPNim, coin game
Greedy seems to workGreedy-reducible DPActivity selection

Given nn items with weights wiw_i and values viv_iAnd a knapsack of capacity WWMaximise the Total value of items selected. Each item can be taken at most once.

dp[i][c] = \max(dp[i-1][c], dp[i-1][c - w_i] + v_i) \quad \mathrm{if c \ge w_i

def knapsack_01(weights, values, capacity):
"""
0/1 knapsack — each item used at most once.
Time: O(n * W)
Space: O(W) with 1D optimisation
"""
n = len(weights)
dp = [0] * (capacity + 1)
for i in range(n):
w, v = weights[i], values[i]
for c in range(capacity, w - 1, -1):
dp[c] = max(dp[c], dp[c - w] + v)
return dp[capacity]

The fundamental idea behind all DP patterns is the same: break a problem into overlapping subproblems, solve each subproblem once, and reuse the results. What changes between patterns is the shape of the subproblems. Linear DP arranges subproblems in a line (like climbing stairs). Interval DP works on ranges (like matrix chain multiplication). Tree DP traverses a tree structure. Bitmask DP represents subsets as bit patterns. The core skill is recognizing which “shape” your problem has, because that determines how you define states and transitions.

The hardest part of DP is in most cases defining the right state — what information do you need to capture at each subproblem? A good rule of thumb is: the state should contain everything that affects future decisions but nothing redundant. For interval DP, the state is (left, right) because the optimal solution for a range depends only on that range. For tree DP, you process children before parents (post-order) because a node’s optimal value depends on its subtree. For bitmask DP, the bitmask itself is the state — it tells you exactly which elements have been used.

Once you have the right state, the transition is in most cases straightforward: try all possible “last moves” and take the best one. The key to efficiency is recognizing when you can reduce the state space: rolling arrays when you only need the previous row, coordinate compression when values are large but few, and sparse representations when most states are unreachable. And always, always check your base cases by hand before coding — a missing or wrong base case is the most common DP bug.