Recognising which DP pattern applies to a problem is the key skill. This section provides a decision Framework.
Iterate forward, `dp[c - w]` may already include item $i$Violating the 0/1 constraint. This is the Most common bug in knapsack implementations.Each item can be taken unlimited times. Iterate capacity forward.
def knapsack_unbounded ( weights , values , capacity ):
Unbounded knapsack — items can be reused.
dp = [ 0 ] * (capacity + 1 )
for c in range ( 1 , capacity + 1 ):
for w, v in zip (weights, values):
dp[c] = max (dp[c], dp[c - w] + v)
Each item can be taken at most k i k_i k i times. Convert to 0/1 knapsack by binary decomposition.
def knapsack_bounded ( weights , values , counts , capacity ):
Bounded knapsack — each item has a maximum count.
Uses binary decomposition to convert to 0/1 knapsack.
Time: O(W * sum(log k_i))
expanded_w, expanded_v = [], []
for w, v, k in zip (weights, values, counts):
expanded_w.append(w * power)
expanded_v.append(v * power)
expanded_w.append(w * amt)
expanded_v.append(v * amt)
return knapsack_01(expanded_w, expanded_v, capacity)
Given a set of integers, determine if a subset sums to a target.
def subset_sum ( nums , target ):
Check if any subset sums to target.
dp = [ False ] * (target + 1 )
for t in range (target, num - 1 , - 1 ):
dp[t] = dp[t] or dp[t - num]
Check if array can be partitioned into two equal-sum subsets.
Time: O(n * sum(nums) / 2)
return subset_sum(nums, total // 2 )
When there are multiple constraints (e.g., weight and volume), the DP state has multiple dimensions.
def knapsack_2d ( weights , volumes , values , max_weight , max_volume ):
2D knapsack with weight and volume constraints.
Time: O(n * max_weight * max_volume)
Space: O(max_weight * max_volume)
dp = [[ 0 ] * (max_volume + 1 ) for _ in range (max_weight + 1 )]
for w, v, val in zip (weights, volumes, values):
for c in range (max_weight, w - 1 , - 1 ):
for d in range (max_volume, v - 1 , - 1 ):
dp[c][d] = max (dp[c][d], dp[c - w][d - v] + val)
return dp[max_weight][max_volume]
Interval DP solves problems on contiguous subarrays/substrings by considering all possible Partitions of an interval.
Given matrices A 1 , A 2 , … , A n A_1, A_2, \ldots, A_n A 1 , A 2 , … , A n where A i A_i A i has dimensions p i − 1 × p i p_{i-1} \times p_i p i − 1 × p i Find the Parenthesisation that minimises the total number of scalar multiplications.
d p [ i ] [ j ] = min i ≤ k < j ( d p [ i ] [ k ] + d p [ k + 1 ] [ j ] + p i − 1 ⋅ p k ⋅ p j ) dp[i][j] = \min_{i \le k \lt j} (dp[i][k] + dp[k+1][j] + p_{i-1} \cdot p_k \cdot p_j) d p [ i ] [ j ] = min i ≤ k < j ( d p [ i ] [ k ] + d p [ k + 1 ] [ j ] + p i − 1 ⋅ p k ⋅ p j )
def matrix_chain_order ( p ):
Optimal parenthesisation of matrix chain multiplication.
dp = [[ 0 ] * (n + 1 ) for _ in range (n + 1 )]
split = [[ 0 ] * (n + 1 ) for _ in range (n + 1 )]
for length in range ( 2 , n + 1 ):
for i in range ( 1 , n - length + 2 ):
cost = dp[i][k] + dp[k + 1 ][j] + p[i - 1 ] * p[k] * p[j]
def build_parenthesis ( i , j ):
return f "( { build_parenthesis(i, split[i][j]) } x { build_parenthesis(split[i][j] + 1 , j) } )"
return dp[ 1 ][n], build_parenthesis( 1 , n)
Given numsWhere nums[i] is the value of the i i i -th balloon, bursting balloon i i i yields nums[left] * nums[i] * nums[right] coins where left and right are the adjacent unburst Balloons. Maximise total coins.
def burst_balloons ( nums ):
Maximize coins from bursting balloons.
Key insight: think about which balloon to burst LAST, not first.
padded = [ 1 ] + nums + [ 1 ]
dp = [[ 0 ] * (n + 2 ) for _ in range (n + 2 )]
for length in range ( 1 , n + 1 ):
for left in range ( 1 , n - length + 2 ):
right = left + length - 1
for k in range (left, right + 1 ):
coins = padded[left - 1 ] * padded[k] * padded[right + 1 ]
dp[left][k - 1 ] + coins + dp[k + 1 ][right]
Given n n n piles of stones and an integer k k k Merge adjacent piles into one pile. Each merge of k k k Piles costs the sum of those k k k piles. Find the minimum total cost, or return -1 if impossible.
def merge_stones ( stones , k ):
Minimum cost to merge stones with exactly k piles per merge.
Possible iff (n - 1) % (k - 1) == 0
if (n - 1 ) % (k - 1 ) != 0 :
prefix[i + 1 ] = prefix[i] + stones[i]
dp = [[ 0 ] * n for _ in range (n)]
for length in range (k, n + 1 ):
for i in range (n - length + 1 ):
for m in range (i, j, k - 1 ):
dp[i][j] = min (dp[i][j], dp[i][m] + dp[m + 1 ][j])
if (j - i) % (k - 1 ) == 0 :
dp[i][j] += prefix[j + 1 ] - prefix[i]
def interval_dp_template ( arr ):
Generic interval DP template.
Time: O(n^3) or O(n^2) depending on transition
dp = [[ 0 ] * n for _ in range (n)]
for length in range ( 2 , n + 1 ):
for left in range (n - length + 1 ):
right = left + length - 1
for mid in range (left, right):
dp[left][mid] + dp[mid + 1 ][right] + cost(left, mid, right)
Tree DP applies dynamic programming on tree structures, using post-order traversal (process children before parent).
def tree_diameter ( n , edges ):
Diameter of a tree (longest path).
from collections import defaultdict
graph = defaultdict( list )
for neighbor in graph[node]:
depth = dfs(neighbor, node)
diameter = max (diameter, max1 + max2)
Maximum path sum in a binary tree.
A path is any node-to-node sequence (not necessarily through root).
Space: O(h) recursion stack
left = max ( 0 , dfs(node.left))
right = max ( 0 , dfs(node.right))
result = max (result, node.val + left + right)
return node.val + max (left, right)
def max_independent_set ( n , edges ):
Maximum independent set on a tree.
An independent set has no two adjacent nodes.
from collections import defaultdict
graph = defaultdict( list )
for neighbor in graph[node]:
child_inc, child_exc = dfs(neighbor, node)
exclude += max (child_inc, child_exc)
Rerooting solves the problem: compute some property for every node as the root.
def rerooting_dp ( n , edges ):
Compute DP value for every node as root.
from collections import defaultdict, deque
graph = defaultdict( list )
children = [[] for _ in range (n)]
for neighbor, w in graph[node]:
if not visited[neighbor]:
children[node].append((neighbor, w))
for node in reversed (order):
for child, w in children[node]:
down[node] += down[child] + w
total_children = sum (down[c] + w for c, w in children[node])
for child, w in children[node]:
up[child] = up[node] + (total_children - (down[child] + w))
result = [down[i] + up[i] for i in range (n)]
Bitmask DP uses bitmasks to represent subsets, enabling O ( 2 n ⋅ n ) O(2^n \cdot n) O ( 2 n ⋅ n ) solutions for problems with Small n n n ( n ≤ 20 n \le 20 n ≤ 20 ).
Find the shortest Hamiltonian cycle visiting all cities exactly once.
d p [ m a s k ] [ i ] = min j ∈ m a s k , j ≠ i ( d p [ m a s k ∖ { i } ] [ j ] + d i s t [ j ] [ i ] ) dp[mask][i] = \min_{j \in mask, j \ne i} (dp[mask \setminus \{i\}][j] + dist[j][i]) d p [ ma s k ] [ i ] = min j ∈ ma s k , j = i ( d p [ ma s k ∖ { i }] [ j ] + d i s t [ j ] [ i ])
Travelling Salesman Problem.
n must be small (in standard practice n <= 20).
dp = [[ INF ] * n for _ in range ( 1 << n)]
for mask in range ( 1 , 1 << n):
if not (mask & ( 1 << u)):
new_mask = mask | ( 1 << v)
dp[new_mask][v] = min (dp[new_mask][v], dp[mask][u] + dist[u][v])
result = min (result, dp[full_mask][u] + dist[u][ 0 ])
Assign n n n workers to n n n tasks, minimising total cost.
Minimum cost assignment problem.
dp = [ float ( ' inf ' )] * ( 1 << n)
for mask in range ( 1 << n):
worker = bin (mask).count( ' 1 ' )
if not (mask & ( 1 << task)):
new_mask = mask | ( 1 << task)
dp[new_mask] = min (dp[new_mask], dp[mask] + cost[worker][task])
Digit DP counts numbers in a range that satisfy certain digit-based properties by processing digits From most significant to least significant.
Count numbers in [lo, hi] satisfying a property.
Time: O(digits * states * 10)
Space: O(digits * states)
digits = list ( map ( int , str (n)))
def dfs ( pos , tight , state ):
return 1 if is_valid(state) else 0
limit = digits[pos] if tight else 9
for d in range (limit + 1 ):
new_state = transition(state, d)
total += dfs(pos + 1 , tight and d == limit, new_state)
return dfs( 0 , True , initial_state())
return count(hi) - count(lo - 1 )
def count_no_consecutive_ones ( n ):
Count numbers from 0 to n with no consecutive 1s in binary.
def dfs ( pos , tight , prev_one ):
limit = int (s[pos]) if tight else 1
for d in range (limit + 1 ):
total += dfs(pos + 1 , tight and d == limit, d == 1 )
return dfs( 0 , True , False )
Minimum number of insertions, deletions, and substitutions to transform one string into another.
dp[i][j] = \begin{cases} dp[i-1][j-1] & \mathrm{if s[i] = t[j] \\ 1 + \min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) & \mathrm{otherwise \end{cases}
Levenshtein distance between two strings.
Space: O(min(m, n)) with 1D optimisation
prev = list ( range (n + 1 ))
for i in range ( 1 , m + 1 ):
for j in range ( 1 , n + 1 ):
curr[j] = 1 + min (prev[j], curr[j - 1 ], prev[j - 1 ])
Longest common subsequence.
for i in range ( 1 , m + 1 ):
for j in range ( 1 , n + 1 ):
curr[j] = prev[j - 1 ] + 1
curr[j] = max (prev[j], curr[j - 1 ])
def longest_palindromic_subsequence ( s ):
Longest palindromic subsequence.
dp = [[ 0 ] * n for _ in range (n)]
for length in range ( 2 , n + 1 ):
for i in range (n - length + 1 ):
dp[i][j] = dp[i + 1 ][j - 1 ] + 2 if length > 2 else 2
dp[i][j] = max (dp[i + 1 ][j], dp[i][j - 1 ])
Regex matching with '.' and '*'.
for j in range ( 2 , n + 1 ):
for i in range ( 1 , m + 1 ):
new_dp = [ False ] * (n + 1 )
for j in range ( 1 , n + 1 ):
new_dp[j] = new_dp[j - 2 ]
if p[j - 2 ] == s[i - 1 ] or p[j - 2 ] == ' . ' :
new_dp[j] = new_dp[j] or dp[j]
elif p[j - 1 ] == s[i - 1 ] or p[j - 1 ] == ' . ' :
Every impartial game (where the available moves depend only on the position, not on which player is Moving) is equivalent to a Nim heap. The Sprague-Grundy theorem states that the Grundy number (mex Of children’s Grundy numbers) determines the winning strategy.
def grundy_number ( positions ):
Compute Grundy number as mex of reachable positions.
mex = minimum excluded value.
for move in get_moves(positions):
reachable.add(grundy_number(move))
Determine winner of Nim game.
Winner: first player iff XOR of all heaps != 0.
Two players alternate taking coins from either end.
Max amount the first player can collect.
dp = [[ 0 ] * n for _ in range (n)]
for length in range ( 2 , n + 1 ):
for i in range (n - length + 1 ):
coins[i] + min (dp[i + 2 ][j] if i + 2 <= j else 0 ,
dp[i + 1 ][j - 1 ] if i + 1 <= j - 1 else 0 ),
coins[j] + min (dp[i + 1 ][j - 1 ] if i + 1 <= j - 1 else 0 ,
dp[i][j - 2 ] if i <= j - 2 else 0 )
Some DP problems have greedy solutions that are simpler and faster. The key question: does making The locally optimal choice always lead to the globally optimal solution?
Problem Greedy Works? Greedy Strategy Fractional knapsack Yes Sort by value/weight ratio Activity selection Yes Earliest finish time Huffman coding Yes Merge two smallest frequencies Minimum spanning tree Yes Kruskal / Prim Dijkstra (non-negative) Yes Process smallest distance 0/1 knapsack No DP required Partition equal subset sum No DP required Edit distance No DP required
Technique When to Use Example Coordinate compression Large coordinate values, few unique sorted(set(values)) + binary searchDifference encoding State depends on differences dp[i][diff] instead of dp[i][a][b]Rolling array Only previous row/column needed prev and curr arraysBitmask Small set of choices (n <= 20) TSP, assignment Sparse DP Many states unreachable Dictionary instead of array
The fill order must respect the dependency: if dp[i] depends on dp[j]Then j must be computed Before i. For interval DP, always iterate by increasing interval length. For 1D DP, verify whether Forward or backward iteration is needed (0/1 knapsack needs backward, unbounded needs forward).
DP values can grow exponentially (e.g., counting paths in a grid). Use Python’s arbitrary-precision Integers, or in C++/Java, use long long or BigInteger. Always check whether the problem asks for The result modulo some value.
In bitmask DP, mask & (1 << i) tests whether bit i i i is set. mask | (1 << i) sets bit i i i . mask & ~(1 << i) clears bit i i i . Forgetting the parentheses around 1 << i in ~(1 << i) is a Common bug because ~ has lower precedence than &.
Missing or incorrect base cases are the most common DP bug. For interval DP, the base case is dp[i][i]. For tree DP, the base case is the leaf node. For string DP, the base case is the empty String. Always verify base cases by hand before writing the transition.
A subsequence is not necessarily contiguous; a subarray (or substring) is. “Longest increasing Subsequence” uses DP over subsequences (O ( n 2 ) O(n^2) O ( n 2 ) or O ( n log n ) O(n \log n) O ( n log n ) ). “Maximum sum subarray” uses Kadane’s algorithm (O ( n ) O(n) O ( n ) ). Mixing these up leads to incorrect solutions.
In digit DP, the tight flag indicates whether the prefix matches the upper bound. When tight is False, you can use any digit (0-9). When tight is True, you can only use digits up to the current Digit of the upper bound. Forgetting to propagate tight correctly produces wrong counts.
Tree DP solutions often assume a specific root ( node 0). If the problem asks for a Property of the tree regardless of root (e.g., diameter), make sure the solution does not depend on The root choice. For problems that require computing a value for every node as root, use rerooting DP.
In game theory DP, the standard formulation computes the value that the current player can Guarantee. A common mistake is to assume both players play optimally for the same objective. In Zero-sum games, player 1 maximises while player 2 minimises. Getting this wrong inverts the result.
This topic covers the core concepts of dynamic programming patterns, including underlying theory, practical implementation, and key applications.
Key concepts include:
variables, data types, and control flow functions and procedures object-oriented programming error handling and debugging modular design Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
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.