The classic problems are worth knowing less for themselves than for the patterns they introduce: linear recurrences, state machines, prefix DP with a validity predicate, inverting the DP (answer becomes an index), sorting to fix an order, and inclusion-exclusion over special cells.

Linear recurrences and tilings

CountRecurrence / value
steps of size (ordered) (sliding window sum)
steps from a set
domino tilings
domino tilings (, odd : 0)
with dominoes and L-trominoes
binary strings without two adjacent 1s
compositions of into parts
domino tilings (general)profile DP (Profile DP) or Kasteleyn’s formula

For huge : matrix power or Kitamasa / Bostan–Mori (Linear Recurrences). Plain Fibonacci has a direct via fast doubling, no matrix needed: , . If no recurrence is obvious, brute-force 20–50 terms, run Berlekamp–Massey, and check OEIS.

VariantChange to the recurrence
cost instead of count (or ) in place of — shortest path on a line
cannot repeat the same step twice in a rowadd a “last step used” dimension
some step sizes are temporarily unusableskip those terms for the affected

Kasteleyn’s formula above is a special case of a general fact: counting perfect matchings of a planar graph is a Pfaffian (hence a determinant), computable in polynomial time — the FKT algorithm. Counting perfect matchings of a general graph is p-hard; planarity is exactly what makes the tiling count tractable in closed form.

State-machine DP: stock trading

States “holding / not holding” × “transactions used”; every extra rule is either a new state or an edge cost, and once the machine is drawn the code is mechanical:

RuleChange
cooldown of 1 day after sellingadd a cooldown state
transaction fee subtract on each sell
must hold days before sellingindex by days held so far
short selling allowedadd a symmetric short state
ll maxProfit(const vector<ll>& p, int k) {                 // at most k buy-sell pairs
    int n = sz(p);
    if (2 * k >= n) { ll s = 0; for (int i = 1; i < n; i++) s += max(0LL, p[i] - p[i - 1]); return s; }
    vector<ll> buy(k + 1, LLONG_MIN / 2), sell(k + 1, 0);
    for (ll x : p) for (int j = 1; j <= k; j++) buy[j] = max(buy[j], sell[j - 1] - x), sell[j] = max(sell[j], buy[j] + x);
    return sell[k];
}
  • Unlimited transactions: sum of positive day-to-day differences.
  • Huge : profit is concave in , so use the Aliens trick (Aliens Trick), or the greedy with a heap of local valleys/peaks merging cheapest pairs in .
  • “Buy at most one unit per day, sell any time, maximise profit” (CF 865D): push each price into a min-heap; if the current price beats the top, take the profit and push the price twice (one copy for “undo this sale later”). This is a regret greedy (Greedy).

Prefix DP with a validity predicate

dp[i] from all dp[j] with s[j..i) valid: word break, palindrome partitioning, decoding digit strings, text justification. The cost is in testing validity:

  • dictionary words: walk a trie from every reachable , or run Aho–Corasick and follow dictionary suffix links (Aho–Corasick);
  • palindromes: precompute pal[j][i] in , or use the eertree’s series links for min-cut palindrome partition (Eertree);
  • convex segment costs (line breaking): Monge, so 1D1D optimisation applies (Monotone Optimizations).
int minPalCuts(const string& s) {                           // fewest palindromes
    int n = sz(s); vector<vector<char>> pal(n, vector<char>(n)); vector<int> dp(n + 1, INT_MAX); dp[0] = 0;
    for (int i = n - 1; i >= 0; i--) for (int j = i; j < n; j++) pal[i][j] = s[i] == s[j] && (j - i < 2 || pal[i + 1][j - 1]);
    for (int i = 1; i <= n; i++) for (int j = 0; j < i; j++) if (pal[j][i - 1]) dp[i] = min(dp[i], dp[j] + 1);
    return dp[n];
}

Counting segmentations can be exponential, so always count modulo something; enumerate only when the output is bounded.

Enumerating all segmentations

The number of segmentations can be exponential ("aaaa...a" with dictionary {a, aa}). A memoised DFS that returns a list of strings blows up on such inputs even though the DFS itself is memoized — the blow-up is in the output size, not the recursion. Count with a DP instead, and only enumerate when the problem bounds the output.

Other prefix-DP-over-a-string shapes: decode ways ( from and , digits to letters), concatenated words (word break with a length constraint), each dictionary word usable at most once (NP-hard in general — this breaks the simple prefix recurrence), allow at most unknown/wildcard characters (add a dimension for the count used).

Inverting the DP: egg dropping

eggs, floors, minimise worst-case drops. Instead of best[k][n], compute = the most floors distinguishable with eggs and drops:

int eggDrop(int k, ll n) {
    vector<ll> f(k + 1, 0); int t = 0;
    while (f[k] < n) { t++; for (int e = k; e >= 1; e--) f[e] = min(n, f[e] + f[e - 1] + 1); }
    return t;
}

With 1 egg the answer is (a linear scan from the bottom — no eggs to spare for binary search). With 2 eggs the answer is the least with (14 for 100 floors); with eggs it is binary search. The 2-egg optimum is achieved by decreasing gaps: for , drop first at floor 14, then (if it survives) 27, then 39, … — never floor 50, because each break costs both an egg and a drop, so the next interval must shrink by one. The same inversion turns knapsack by capacity into knapsack by value, and “minimum operations to reach ” into “maximum reach with operations”.

Variants needing a different DP, not just a bigger table: minimising the expected number of drops (a DP over probability distributions, not the worst case); eggs with different individual fragility thresholds; counting the number of distinct strategies using exactly drops and eggs (the binomial sum above, read as a count rather than a bound).

Grid paths with forbidden cells

Paths from to moving right/down avoiding forbidden cells ( up to , ): sort the cells (and append the target), and let = number of paths from the start to cell that avoid all earlier forbidden cells:

(CF 559C). The same “first bad event” decomposition counts permutations avoiding patterns, sequences avoiding prefix conditions, and lattice paths not touching a set.

Ordering first, then DP

Many subset-selection problems become knapsack once the right processing order is fixed by an exchange argument:

ProblemOrderDP
jobs with durations and deadlines , maximise number/value donedeadline ascendingdp[time] = best value, knapsack with capacity for item
boxes stacked, each with weight and strength (load on it )by descending (top-down)dp[load]
tasks with requirement and cost (need to start, spend )by descendingknapsack over remaining resource
nested boxes / envelopessort one coordinateLIS on the other (LIS)

More patterns

  • No two adjacent chosen, on a cycle: run the line DP twice, once forcing the first element unused and once forcing the last unused.
  • Two simultaneous walkers on a grid (collect values on two disjoint-ish paths): parametrise by step and both rows, ; count a shared cell once.
  • Maximum-sum submatrix: fix two rows, Kadane over column sums, .
  • Largest all-ones square: ; largest rectangle uses histograms (Monotonic Stack).
  • DP over prefix with “last occurrence”: number of distinct subsequences, .
  • Max-min segmentation: binary search the answer and check greedily, no DP needed.