A DP is a shortest/longest path or a sum over paths in an acyclic state graph. Designing one means picking states (what about the past affects the future), transitions (edges), and an order (a topological order of the states). Time = states × transition cost, and every optimisation in this chapter attacks one of those two factors.

When DP does not apply

  • No optimal substructure. Longest simple path in a general graph — an optimal path to may use vertices a longer path to needs, so smaller subproblems don’t combine into the bigger one. (On a DAG it works, since there’s no choice of which vertices to “save” for later.)
  • Too many states with no compression. If the state genuinely needs the full set of visited vertices and , no reduction trick shrinks — look for a different model, not a bigger machine.
  • Greedy already settles it. Don’t build a DP for a problem an exchange argument proves optimal for — see Exchange Arguments; a DP that happens to also get the right answer is strictly more code and more bug surface for no gain.
  • Only overlapping subproblems, no optimal substructure: that’s memoised search (e.g. counting paths in a DAG with cycles removed by state, game-tree search) — correct, but “optimal substructure” isn’t the reason it works, so don’t cite it as justification.

Designing the state

The state must hold everything about the past that affects the future, and nothing more.

  • Ask: “to finish the job, what would someone need to be told?”
  • Sort first so a dimension becomes implicit (intervals by end, items by deadline, edges by weight).
  • Swap value and dimension: replace dp[i][x] = feasible? with dp[i] = best x when feasibility is monotone in (LIS tails, “minimum weight for value ” knapsack when values are small).
  • Keep only a summary: “was the last element chosen”, “last value modulo ”, “how many open groups”, “the multiset of open things up to symmetry”.
  • Count objects up to relabeling: “set of used items” becomes “number of used items” when the items are interchangeable.
  • Contribution instead of configuration: if the cost is a sum over pairs/edges, decide each contribution at the moment it’s determined (e.g. “open brackets” style DPs: track how many segments are still open and pay for all of them per step).
  • Defer decisions (“exchange the order of summation”): decide only what is needed now, keep a count of pending choices, and resolve them later (connected-component DP, insertion DP).
  • Fix the extremal element: iterate over the position or value of the maximum/minimum and split the problem around it (Cartesian-tree DP, interval DP over the last removed element).
Problem smells likeState
prefix decisionsdp[i], dp[i][last], dp[i][used]
two sequencesdp[i][j]
contiguous range, merge/remove operationsdp[l][r] (sometimes dp[l][r][extra])
budgetdp[i][w] or dp[i][value] = min weight
subtreedp[v][...]
subset, dp[mask], dp[mask][last]
digitsdp[pos][tight][started][...]
grid, narrow widthdp[cell][profile]
process over time with a few countersdp[t][counters]
two sequences, no-two-adjacent-chosendp[i][j][0/1] (first elements, chosen, last taken or not) — the move from “the set of chosen elements” to “was the previous one chosen” is the archetypal state reduction: a problem with naive state dp[i][k][\text{mask of chosen}] collapses to states, transition, once you notice only the previous choice, not the whole chosen set, affects feasibility of the next one

Named examples by shape: dp[i] — LIS, house robber, coin change, word break; dp[i][j] — LCS, edit distance, string alignment; dp[l][r] — matrix chain, burst balloons, palindrome partitioning; dp[v][0/1] — independent set on a tree, tree colouring; dp[mask][i] — Held–Karp (TSP); dp[row][profile] — broken-profile DP.

Feasibility by state count

Total workFeasible (1-2 s)
, –
–
–
–
–
–
–

Memoisation vs tabulation

Top-down (memo)Bottom-up (table)
orderimplicitmust be derived
visitsonly reachable statesall states
speedfunction calls (2–5× slower)tight loops, vectorisable
memory trickshardrolling arrays, bitsets
depthstack overflow risk at –none
best forsparse or weird states (hash of a board, tree hashes)regular grids of states

Use a separate vis array (or version stamps for many test cases: if (stamp[s] == tc) return memo[s]) rather than a sentinel value that might be a real answer.

Non-numeric states: anything hashable works as a memo key — strings, tuples, canonical board positions, tree hashes. This is where top-down genuinely beats bottom-up: there is often no sensible way to enumerate such states in a fixed order, but there is always a way to recurse into them. Game solving, puzzle search, and “count distinct configurations up to symmetry” problems live here.

BugSymptom
Caching before the base-case checkwrong answers on trivial inputs
A parameter that affects the answer but isn’t in the keywrong answers, not reproducible
A parameter in the key that doesn’t affect the answercorrect but far too slow
Forgetting to reset between test casesfirst test right, rest wrong
Sentinel value collides with a real answersilent recomputation or wrong answer
Recursion on a cyclic state graphinfinite loop — needs Dijkstra/Bellman-Ford, not memoization

Memoization silently assumes the state graph is acyclic (the proof at the top of this note requires a topological order to exist). If a state can depend on itself, directly or through a chain, memoization loops forever or returns garbage.

Order and direction

RecurrenceLoop order
dp[i] from smaller i increasing
dp[l][r] from strictly shorter intervalsby length, or decreasing then increasing
treepost-order (reverse BFS order works)
dp[mask] from submasksmask increasing
0/1 knapsack in one arraycapacity decreasing
unbounded knapsack in one arraycapacity increasing
combinations (order irrelevant)items outer, amount inner
sequences (order matters)amount outer, items inner

Push vs pull: pull (dp[i] = f(dp[j], j < i)) mirrors the recurrence; push (dp[j] += ... from dp[i]) is easier when successors are simple (grid moves, “add one item”). Both visit the same edges.

Cache locality for large tables: for a 2D table, iterate the last index in the inner loop so memory access is sequential. Swapping the loop order on a table can cost a 5-10x slowdown from cache misses alone — enough to turn an accepted solution into a TLE, with no change to the asymptotic complexity.

Space and reconstruction

  • Rolling rows: keep the last layers if the recurrence looks back layers.
  • Bitsets for boolean DPs: less time and memory (Bitset).
  • Reconstruction without a choice table: walk back from the answer and re-check which transition attains the stored value.
  • Reconstruction in linear memory: Hirschberg-style divide and conquer, i.e. compute the forward DP to the middle layer and the backward DP from the end, find where the optimal path crosses the middle, recurse on both halves; time, memory (Sequence DP).
  • Checkpointing: store every -th layer, recompute blocks during traceback: memory, 2× time.
TypeElements in 256 MB
char / bool
short
int / float
long long / double
bits (bitset)

A long long table of is 200 MB — too big for most limits. The same table as int is 100 MB — borderline. Rolled to two rows, it’s 40 KB.

vector<bool> is not an array of bool

It’s a bit-packed specialisation: 8x smaller but slower per access, and it can’t give you a pointer to an element. For a DP table it’s usually the right memory trade-off; for hot inner loops prefer bitset (faster, fixed size) or vector<char>.

Optimisation map

Transition shapeConditionTechniqueCost
sliding windowmonotone deque (Monotone)
any rangessegment tree / sparse table on
linear in a parameterCHT / Li Chao (CHT)–
over layersmonotone argmin / MongeD&C opt (D&C)
quadrangle inequalityKnuth (Knuth)
, Monge1D1Dbinary-search queue, SMAWK, LARSCH /
exactly parts, answer convex in concavityAliens / WQS (Aliens)
is convex piecewise linear in a valueconvexityslope trick (Slope Trick)
linear recurrence, constant transitions, huge linearitymatrix power / Berlekamp–Massey (Matrix DP) /
point updates to the input, re-answerDP is a monoid productdynamic DP (Dynamic DP) per update
sum over submasks—SOS / subset convolution (SOS) /
polynomial-like counting DPgenerating functionspower series (Power Series)
knapsack with small item costsbounded weightsweight-capped arguments, (max,+) concave convolution (Knapsack)varies

Checklist

  1. Complete state? Is anything outside the state influencing the future?
  2. Minimal state? Is any dimension determined by the others?
  3. Unreachable states at (not 0); never underflows.
  4. Every dependency computed before use.
  5. Answer: dp[n] or the best over the last layer?
  6. Multiple test cases: reset only what was touched.
  7. Before submitting, compare with a brute force on random small inputs (Stress Testing).