State = a contiguous range . Transitions either split at some (dp[l][k] and dp[k][r]), or peel an end (dp[l+1][r], dp[l][r-1]). states × splits = , fine for –. Iterate by increasing length (or decreasing, increasing) so both halves are ready.
for (int len = 2; len <= n; len++)
for (int l = 0, r = len; r <= n; l++, r++) {
dp[l][r] = INF;
for (int k = l + 1; k < r; k++) dp[l][r] = min(dp[l][r], dp[l][k] + dp[k][r] + cost(l, k, r));
}Catalogue
| Problem | Recurrence |
|---|---|
| matrix chain () | |
| merging adjacent piles (cost = merged size) | split, cost ; Knuth makes it ; Garsia–Wachs |
| optimal BST with access frequencies | split at the root, cost ; Knuth (Knuth) |
| convex polygon triangulation | split at the apex of the triangle on edge ; cost is the triangle’s weight (area, perimeter, whatever the problem charges) |
| burst balloons | choose the balloon burst last: (exclusive bounds) |
| longest palindromic subsequence | peel: ? : |
| minimum insertions to make a palindrome | |
| take from either end, two players, maximise difference | |
| strange printer (min strokes to paint a string) | merge with an equal character, below |
| removing boxes ( per block) | add a dimension: extra equal boxes attached on the right |
| longest regular bracket subsequence / colouring brackets | split at the partner of |
| context-free grammar membership (CYK) | split, over nonterminals |
| circular arrays | double the array, answer (cost becomes from — check the limits before committing) |
| zero-out a range with “decrement a subarray” moves | , no DP needed |
Choosing the last operation
When the first decision makes the two sides interact (bursting a balloon makes its neighbours adjacent), decide the last event instead: everything else in the range has already happened, and the boundaries are still present, so the two sides are independent.
Proof: burst balloons recurrence
Pad with and let be the best score for bursting all balloons strictly between and while and are still present. Fix the last balloon burst in . Before it, balloons in and are burst while is present, so balloons on one side never have a neighbour on the other side: the two sides are independent subproblems with boundaries and , scoring and at best. When is burst, its neighbours are exactly and : score . Every order decomposes this way for its last balloon, and every pair of sub-orders combines into a valid order, so the maximum over is the optimum.
Games on an array
= (score of the player to move) − (score of the other) on the remaining range. Taking leaves the opponent to move on with advantage from their point of view:
With an even number of elements, the first player can always guarantee at least half (take all odd or all even positions), but the DP gives the exact value. See Game DP for the general theory this is a special case of.
Proof: the negamax recurrence
The game on is zero-sum in the difference of scores. If the mover takes , their final difference is minus the difference the opponent achieves on playing optimally as the mover there, which is by induction on length. The mover chooses the better of the two moves. “Odd or even positions” works because after the first player takes an end with the chosen parity, both ends available to the second player have the other parity, so the first player can always take the chosen parity again.
Strange printer
Minimum strokes to print , where a stroke paints a contiguous segment with one colour over whatever is there:
int strangePrinter(const string& s) {
int n = sz(s); vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0)); // dp[i][j+1] for s[i..j]
for (int i = n - 1; i >= 0; i--) for (int j = i; j < n; j++) {
int& d = dp[i][j + 1]; d = dp[i][j] + 1;
for (int k = i; k < j; k++) if (s[k] == s[j]) d = min(d, dp[i][k + 1] + dp[k + 1][j]);
}
return n ? dp[0][n] : 0;
}Proof sketch: the printer recurrence
Strokes can be assumed nested or disjoint (laminar), because if two strokes partially overlap, the earlier one can be shortened to the part not covered by the later one. Consider the stroke that gives its final colour. If it covers no other final-colour position in , it can be shrunk to , costing . Otherwise let be the rightmost other position it finally colours; then and the stroke spans , with everything in painted by later strokes nested inside it (), and the stroke can be charged to the solution for (extend the stroke that colours to also cover ). Conversely both expressions describe valid printing plans.
Removing boxes
dp[l][r][k] = best score for boxes when boxes equal to box are glued after :
, fast in practice. The “glued extra items” dimension is the general fix when merging blocks of equal elements is beneficial (Zuma-like problems).
Brackets and grammars
- Longest regular bracket subsequence: .
- Counting colourings of a given regular sequence (each pair: one bracket coloured, adjacent coloured brackets differ): DP over the matching tree, .
- CYK (grammar in Chomsky normal form):
can[l][r][A]iff ; combinecan[l][m][B]andcan[m][r][C]for each rule . ; with bitsets over nonterminals, faster. Counting parse trees is the same loops with sums and products. A general grammar must be converted to CNF first (eliminate and unit rules, binarise any right-hand side longer than 2) before CYK applies.
// rules: term[A] = set of chars, bin = {A, B, C} for A -> B C
bool cyk(const string& s, int K, int start, const vector<vector<char>>& term, const vector<array<int, 3>>& bin) {
int n = sz(s); if (!n) return false;
vector<vector<vector<char>>> can(n, vector<vector<char>>(n + 1, vector<char>(K)));
for (int i = 0; i < n; i++) for (int A = 0; A < K; A++) can[i][i + 1][A] = term[A][(unsigned char)s[i]];
for (int len = 2; len <= n; len++) for (int l = 0; l + len <= n; l++) for (int m = l + 1; m < l + len; m++)
for (auto [A, B, C] : bin) if (can[l][m][B] && can[m][l + len][C]) can[l][l + len][A] = 1;
return can[0][n][start];
}Speedups
| Condition | Technique | Result |
|---|---|---|
| cost satisfies the quadrangle inequality and is monotone on inclusion | Knuth: | |
| merging piles in fixed order (alphabetic trees) | Garsia–Wachs / Hu–Tucker | |
| only “interesting” intervals (e.g. split by the maximum) | DP on the Cartesian tree | – |
| matrix chain | Hu–Shing (convex polygon partitioning) | , rarely needed |
Matrix chain does not satisfy the quadrangle inequality in general, so Knuth’s optimisation is invalid there; it is valid for optimal BST and pile merging. This is a common mix-up — Knuth’s own result was specifically for optimal BSTs, whose cost (sum of frequencies) is Monge, not for matrix chain’s , which isn’t.
When merge order can be chosen freely (the leaves may be permuted, e.g. picking which items to merge first with no fixed left-to-right sequence), plain greedy Huffman coding solves the version directly; when the order is fixed (as in matrix chain, optimal BST, polygon triangulation), you need Hu–Tucker/Garsia–Wachs or a Knuth-optimised DP instead — matrix chain is the hard case precisely because its order is fixed and its cost isn’t Monge.
Polygon triangulation and matrix chain are the same recurrence, and the correspondence is a genuine bijection: each parenthesisation of the matrix product corresponds to a triangulation of a convex -gon, which is why the number of parenthesisations (equivalently, triangulations) is the Catalan number (Catalan Numbers).
Interval DP in the wild: optimising a chain of matrix multiplications, database join-order optimisation (same shape as matrix chain), tensor-contraction ordering in ML frameworks.