SwadhyAI Notes

All DSA notes

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

1

Minimal state

What must the future still know?

2

Count states × transitions

Write the budget before coding.

3

Dependency order

Popcount, length, digit position, postorder.

4

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.

dp[0111][C] = 45
1. Toggle visited cities
2. Choose the 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 ✓

O(n · 2ⁿ)DP states
(n − 1)!fixed-start tour orders

Pattern map: what belongs in the state?

PatternCanonical stateTypical budgetRecognition signal
Bitmask / subsetdp[mask][last]O(2ⁿ · n²)n ≲ 20 and the chosen set changes future choices
Intervaldp[left][right]O(n³) baselineThe last/first operation splits one contiguous range
Digitdp[pos][automaton][tight][started]digits × states × radixCount values in a huge numeric range under digit constraints
Treedp[node][parent-choice]O(V · states²)Removing the parent edge makes child subproblems independent
Profile / broken profiledp[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

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]]) == 0

Java

java
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

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, 20

Java

java
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

OptimizationTransition shapeRequired proof / condition
Rolling arrayRow i reads only a fixed number of previous rowsNo future reconstruction needs overwritten cells, or store parent decisions separately
Monotone dequeSliding-window min/max transitionCandidate window advances monotonically and stale candidates can be removed
Divide-and-conquer DPdp[k][j] = min_i(prev[i] + C(i,j))Prove monotonicity of the optimal split: opt[j] ≤ opt[j+1]
Knuth optimizationInterval split DPQuadrangle inequality plus opt[l][r−1] ≤ opt[l][r] ≤ opt[l+1][r]
Convex hull trick / Li ChaoTransition becomes min/max of lines at query xDerive slope/intercept exactly; monotone deque CHT additionally needs ordered slopes/queries
Bitset DPBoolean reachability with uniform shiftsThe 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

  1. Write the Held–Karp state and transition. For n = 4, how many (mask, last) pairs are reachable from city 0?
  2. A grid is 100×8. Why is profile DP plausible after rotation, and what is the exponential dimension?
  3. 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.