DSA Dynamic Programming
Advanced Dynamic Programming
Brute-force TSP is correct; n! at n = 16 is not. Dual conflict: you need an exact tour on a small city set without enumerating permutations.
Same discipline as intro DP: state first, then transitions. Patterns include , , , and justified accelerations from .
Algorithm journey
Minimal state
What must the future still know?
Count states × transitions
Write the budget before coding.
Dependency order
Popcount, length, digit position, postorder.
CHT, Knuth, deque: name the condition.
What actually happens
Naive idea: enumerate permutations, all splits, or every integer in a huge range. Correct on tiny instances; dead at contest limits.
Better idea: compress history into a mask, interval, digit automaton, or tree rerooting state. Held–Karp replaces n! with about 2ⁿ n² states. Digit DP counts without iterating the range.
The improved design still fails when n exceeds the exponential budget, when you apply Knuth/CHT without the inequality, or when drops a field the future needs.
Invariant
Each filled state equals the optimum (or count) for that compressed history. Transitions may read only strictly smaller states under the well-founded measure (popcount, interval length, digit index).
How to choose an advanced pattern
1. n ≲ 20 and subsets matter? Bitmask DP.
2. Contiguous last operation? Interval DP.
3. Huge numeric range, digit rules? Digit DP. Narrow grid? Profile / broken profile.
Quantitative check
Held–Karp at n = 16: 16 · 2¹⁶ ≈ 10⁶ states, each with ≤16 transitions: on the order of 10⁷ operations. n = 25 is about 800× larger in the 2ⁿ factor alone: usually out of budget without extra structure.
Interactive state-space lab
Bitmask DP dependency lab
Build a Held–Karp state: visited subset + current endpoint.
State key: mask records the set; endpoint records where the partial tour finishes.
Previous mask: 0011 (remove C)
Transition candidates
Take the cheapest previous endpoint, then pay its edge to C.
dp[0011][A] + w(A,C)
Infinity + 15 = Infinity
dp[0011][B] + w(B,C)
10 + 35 = 45 ✓
Pattern map: what belongs in the state?
| Pattern | Canonical state | Typical budget | Recognition signal |
|---|---|---|---|
| Bitmask / subset | dp[mask][last] | O(2ⁿ · n²) | n ≲ 20 and the chosen set changes future choices |
| Interval | dp[left][right] | O(n³) baseline | The last/first operation splits one contiguous range |
| Digit | dp[pos][automaton][tight][started] | digits × states × radix | Count values in a huge numeric range under digit constraints |
| Tree | dp[node][parent-choice] | O(V · states²) | Removing the parent edge makes child subproblems independent |
| Profile / broken profile | dp[row][frontierMask] | O(height · 2ʷ · transitions) | A grid is long but narrow; only the boundary affects the future |
Bitmask DP: Held–Karp TSP
dp[mask][v] is the cheapest path that starts at city 0, visits exactly mask, and ends at v. The transition appends one unvisited city. The table has n·2ⁿ states and at most n outgoing transitions per state, still exponential, but dramatically smaller than enumerating n! tours.
Python
def held_karp(distance: list[list[int]]) -> int:
"""Minimum Hamiltonian cycle starting and ending at vertex 0."""
n = len(distance)
if n == 0:
return 0
if n > 20 or any(len(row) != n for row in distance):
raise ValueError("expected a square matrix with at most 20 cities")
if n == 1:
return 0
infinity = float("inf")
dp = [[infinity] * n for _ in range(1 << n)]
dp[1][0] = 0
for mask in range(1 << n):
if (mask & 1) == 0:
continue
for last in range(n):
if dp[mask][last] == infinity:
continue
for nxt in range(n):
if mask & (1 << nxt):
continue
next_mask = mask | (1 << nxt)
dp[next_mask][nxt] = min(
dp[next_mask][nxt], dp[mask][last] + distance[last][nxt]
)
full = (1 << n) - 1
return min(dp[full][last] + distance[last][0] for last in range(n))
graph = [
[0, 10, 15, 20], [10, 0, 35, 25],
[15, 35, 0, 30], [20, 25, 30, 0],
]
assert held_karp(graph) == 80
assert held_karp([[7]]) == 0Java
import java.util.Arrays;
public class BitmaskTsp {
static long heldKarp(long[][] distance) {
int n = distance.length;
if (n == 0) return 0L;
if (n > 20) throw new IllegalArgumentException("at most 20 cities");
for (long[] row : distance) {
if (row.length != n) throw new IllegalArgumentException("matrix must be square");
}
if (n == 1) return 0L;
long infinity = Long.MAX_VALUE;
long[][] dp = new long[1 << n][n];
for (long[] row : dp) Arrays.fill(row, infinity);
dp[1][0] = 0L;
for (int mask = 0; mask < (1 << n); mask++) {
if ((mask & 1) == 0) continue;
for (int last = 0; last < n; last++) {
if (dp[mask][last] == infinity) continue;
for (int next = 0; next < n; next++) {
if ((mask & (1 << next)) != 0) continue;
int nextMask = mask | (1 << next);
long candidate = Math.addExact(dp[mask][last], distance[last][next]);
dp[nextMask][next] = Math.min(dp[nextMask][next], candidate);
}
}
}
int full = (1 << n) - 1;
long answer = infinity;
for (int last = 0; last < n; last++) {
if (dp[full][last] == infinity) continue;
answer = Math.min(answer, Math.addExact(dp[full][last], distance[last][0]));
}
return answer;
}
public static void main(String[] args) {
long[][] graph = {
{0,10,15,20}, {10,0,35,25}, {15,35,0,30}, {20,25,30,0}
};
if (heldKarp(graph) != 80L || heldKarp(new long[][]{{7}}) != 0L) {
throw new AssertionError();
}
}
}Digit DP: count a range without iterating it
Define F(x) as the count in [0,x]; answer [L,R] with F(R) − F(L−1). A tight flag says whether the prefix still equals the bound. Add started when leading zeros must not trigger the digit property, and an automaton state for constraints such as forbidden substrings or remainder modulo m.
Python
from functools import cache
def count_with_digit_sum(bound: int, target: int) -> int:
if bound < 0 or target < 0:
return 0
digits = [int(ch) for ch in str(bound)]
@cache
def dp(position: int, total: int, tight: bool) -> int:
if total > target:
return 0
if position == len(digits):
return int(total == target)
limit = digits[position] if tight else 9
return sum(
dp(position + 1, total + digit, tight and digit == limit)
for digit in range(limit + 1)
)
return dp(0, 0, True)
def count_range(left: int, right: int, target: int) -> int:
if left < 0 or left > right:
raise ValueError("expected 0 <= left <= right")
return count_with_digit_sum(right, target) - count_with_digit_sum(left - 1, target)
assert count_range(1, 20, 2) == 3 # 2, 11, 20Java
import java.util.Arrays;
public class DigitSumDP {
private final int[] digits;
private final int target;
private final long[][] memo;
private DigitSumDP(long bound, int target) {
this.digits = Long.toString(bound).chars().map(ch -> ch - '0').toArray();
this.target = target;
this.memo = new long[digits.length][target + 1];
for (long[] row : memo) Arrays.fill(row, -1L);
}
private long solve(int position, int sum, boolean tight) {
if (sum > target) return 0L;
if (position == digits.length) return sum == target ? 1L : 0L;
if (!tight && memo[position][sum] != -1L) return memo[position][sum];
int limit = tight ? digits[position] : 9;
long result = 0L;
for (int digit = 0; digit <= limit; digit++) {
result = Math.addExact(result,
solve(position + 1, sum + digit, tight && digit == limit));
}
if (!tight) memo[position][sum] = result;
return result;
}
static long countUpTo(long bound, int target) {
if (bound < 0 || target < 0) return 0L;
if (target > 9 * Long.toString(bound).length()) return 0L;
return new DigitSumDP(bound, target).solve(0, 0, true);
}
static long countRange(long left, long right, int target) {
if (left < 0 || left > right) throw new IllegalArgumentException("bad range");
return Math.subtractExact(countUpTo(right, target), countUpTo(left - 1, target));
}
public static void main(String[] args) {
if (countRange(1, 20, 2) != 3L) throw new AssertionError();
}
}Tree DP and rerooting
Maximum independent set
Root the tree once. If u is chosen, no child may be chosen; otherwise each child takes its better state.
take[u] = weight[u] + Σ skip[child]
skip[u] = Σ max(take[child], skip[child])
Invariant: after postorder processes u, both values are optimal for the entire rooted subtree under the stated choice of u.
Rerooting / all-roots DP
A postorder pass computes child contributions. A preorder pass moves the root across each edge by removing the child's contribution from the parent aggregate and adding the parent's outside contribution to the child.
- • Use prefix/suffix aggregates if “remove one child” has no inverse.
- • Keep parent and child messages separate to avoid using partially updated state.
- • Typical result: all-root answers in O(V), instead of O(V²) rerunning a tree DP.
Interval and profile DP
Interval DP
Fill by increasing interval length so every strict subinterval is ready. Matrix-chain multiplication, burst balloons, optimal BST, and palindrome problems fit this shape.
dp[l][r] = min over k of combine(dp[l][k], dp[k+1][r])
Profile DP
Sweep a narrow grid. The mask records occupied/connected cells on the frontier; everything behind the frontier is summarized and can be forgotten.
Parameter matters: O(H·2^W) is practical only when width W is small. Rotate the grid to minimize the exponential dimension.
Optimization prerequisites, never cargo-cult these
| Optimization | Transition shape | Required proof / condition |
|---|---|---|
| Rolling array | Row i reads only a fixed number of previous rows | No future reconstruction needs overwritten cells, or store parent decisions separately |
| Monotone deque | Sliding-window min/max transition | Candidate window advances monotonically and stale candidates can be removed |
| Divide-and-conquer DP | dp[k][j] = min_i(prev[i] + C(i,j)) | Prove monotonicity of the optimal split: opt[j] ≤ opt[j+1] |
| Knuth optimization | Interval split DP | Quadrangle inequality plus opt[l][r−1] ≤ opt[l][r] ≤ opt[l+1][r] |
| Convex hull trick / Li Chao | Transition becomes min/max of lines at query x | Derive slope/intercept exactly; monotone deque CHT additionally needs ordered slopes/queries |
| Bitset DP | Boolean reachability with uniform shifts | The transition is expressible as word-parallel bit operations |
Correctness, reconstruction, and testing
Induction order
State the well-founded measure: popcount, interval length, postorder, digit position. Then prove each transition reads only smaller solved states.
Witness recovery
Store the winning choice or recompute it from adjacent table values. Space optimization may preserve the score while destroying the witness.
Differential tests
On tiny random inputs, compare with brute force. Include empty/singleton states, ties, impossible states, negative costs, and maximum numeric bounds.
Sentinel safety: never add to language maximum as “infinity.” Guard unreachable states and use Java long with a bounded sentinel or checked arithmetic.
Industry pattern: Viterbi
Viterbi is sequence DP over (time, hidden state). Each transition chooses the most likely previous state and stores a backpointer. Speech recognition, communications decoding, and biological sequence models use this shape. Production versions work in log space to replace underflow-prone probability products with sums.
Exercises
- Write the Held–Karp state and transition. For n = 4, how many (mask, last) pairs are reachable from city 0?
- A grid is 100×8. Why is profile DP plausible after rotation, and what is the exponential dimension?
- Name a prerequisite you must prove before applying Knuth optimization to an interval DP. What goes wrong if you skip it?
Primary sources
- Held, M. & Karp, R. (1962): A dynamic programming approach to sequencing problems.
- Knuth, D. (1980s notes) / Yao: quadrangle inequality and speedups for interval DP.
- Forney, G. D.: The Viterbi algorithm (sequence DP with backpointers).
Takeaway
Advanced DP is still state first. Compress history carefully, count the budget, and accelerate only with a proved transition structure.