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
| Count | Recurrence / 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.
| Variant | Change to the recurrence |
|---|---|
| cost instead of count | (or ) in place of — shortest path on a line |
| cannot repeat the same step twice in a row | add a “last step used” dimension |
| some step sizes are temporarily unusable | skip 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.
Proof: the and tromino recurrences
: let count full tilings and count tilings of a board with one corner cell of the first column already covered. Looking at how the first column is covered: (the left column is covered by one vertical domino plus a horizontal one, in two mirror ways, or by three horizontal dominoes), and . Eliminating gives .
Dominoes and L-trominoes on : classify by the leftmost tile(s): a vertical domino (), two horizontal dominoes (), or an L-tromino starting a “zigzag” chain that ends with another tromino after horizontal dominoes (2 orientations each). This gives ; subtracting the same identity for gives .
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:
| Rule | Change |
|---|---|
| cooldown of 1 day after selling | add a cooldown state |
| transaction fee | subtract on each sell |
| must hold days before selling | index by days held so far |
| short selling allowed | add 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).
Proof: unlimited transactions and the shortcut
Any transaction buying on day and selling on day earns , and non-overlapping transactions use disjoint sets of differences. So any strategy earns at most , and taking one transaction per maximal increasing run achieves it. The number of increasing runs is at most , so with the limit never binds.
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).
Proof: recurrence and closed form
With eggs and drops, drop from some floor. If the egg breaks we must solve the floors below with eggs and drops, so at most floors can be below; if it survives, at most floors above; plus the floor tested. Placing the first drop at exactly floor achieves the bound, so the recurrence is exact. For the closed form, a strategy is a binary decision tree of depth ; the threshold is determined by the leaf reached, and leaves correspond to break/survive strings of length with at most breaks. Formally, satisfies the same recurrence by Pascal’s rule (, and the accounts for the term of the first sum) with the same boundary values .
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.
Proof: first-forbidden-cell decomposition
Every monotone path to cell either avoids all forbidden cells (counted by ) or has a first forbidden cell it visits. Paths whose first forbidden cell is are in bijection with (a path to avoiding forbidden cells, counted by ) × (any path from to , a binomial). These classes are disjoint and exhaust all paths to , giving the formula. Sorting by ensures every that can precede is processed first.
Ordering first, then DP
Many subset-selection problems become knapsack once the right processing order is fixed by an exchange argument:
| Problem | Order | DP |
|---|---|---|
| jobs with durations and deadlines , maximise number/value done | deadline ascending | dp[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 descending | knapsack over remaining resource |
| nested boxes / envelopes | sort one coordinate | LIS on the other (LIS) |
Proof: earliest deadline first is a valid order for any chosen set
Suppose a set of jobs can be scheduled to meet all deadlines, and two consecutive jobs are done with . Swapping them keeps the finishing time of the pair the same; now finishes earlier, and finishes at the old finishing time of , which was . So both still meet their deadlines. Bubble-sorting by deadline therefore preserves feasibility, and checking a set only in deadline order loses nothing. The other orders follow from the same adjacent-swap computation (for boxes: the pair is safe in order above iff the load condition holds, and comparing the two orders reduces to comparing ).
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.