Items with cost and value , capacity . The basic DPs are ; the real skill is noticing which quantity is small (capacity, total value, number of items, maximum weight, number of distinct weights) and which structure the items have (equal weights give concave groups, bounded total gives distinct sizes).
The family
| Variant | Recurrence / method | Cost |
|---|---|---|
| 0/1 | for w = W..c: dp[w] = max(dp[w], dp[w-c] + v) | |
| unbounded | same, capacity increasing | |
| bounded ( copies) | binary splitting, or monotone deque per residue | / |
| exactly | init , others | same |
| small total value | dp[value] = min weight | |
| boolean (subset sum) | `bitset | = bitset << c` |
| subset sum, total bounded | merge duplicates: items | |
| counting subsets | dp[w] += dp[w-c]; reversible (deletions) | |
| many items with equal weight | concave max-plus convolution per residue | per weight class |
| small weights , huge | greedy prefix + DP on exchanges | |
| huge everything, | meet in the middle (MITM) | |
| representability of huge with unlimited coins | shortest paths modulo the smallest coin | |
| items on a tree (take parent to take child) | tree knapsack (Tree DP) | or |
| fractional | greedy by density | |
| -approximation | round values to multiples of , value-indexed DP | |
| minimum items to reach exactly | dp[w] = min(dp[w-c]+1) | |
| two constraints (weight and volume) | dp[w][vol] | |
| independent knapsacks | dp[w_1][w_2]...[w_k] — exponential in |
Loop directions and why they work: DP Foundations. The basic 0/1 DP is only pseudo-polynomial: it’s polynomial in the value of , not in the number of bits needed to write — which is exactly why knapsack is NP-hard despite this simple algorithm, and exactly why an FPTAS (below) exists for it but not for strongly NP-hard problems.
Proof: fractional knapsack greedy and the FPTAS bound
Greedy: if an optimal fractional solution takes some amount of a lower-density item while a higher-density item is not fully taken, moving weight from the former to the latter changes the value by . Repeating gives the density-ordered solution without loss.
FPTAS: with and , the optimum for the original values satisfies (since , assuming every item fits alone). The solution found is optimal for , so its original value is at least of itself . Total scaled value is at most , giving states.
Bounded knapsack in
For an item and a residue , write . Then , a sliding-window maximum.
void addBounded(vector<ll>& dp, int c, ll v, ll k) { // dp over capacities 0..W, in place
int W = sz(dp) - 1; vector<ll> old = dp;
for (int r = 0; r < c && r <= W; r++) {
deque<pair<ll, ll>> dq; // (s, old[r + s c] - s v), values decreasing
for (ll t = 0, j = r; j <= W; t++, j += c) {
ll val = old[j] - t * v;
while (sz(dq) && dq.back().second <= val) dq.pop_back();
dq.push_back({t, val});
if (dq.front().first < t - k) dq.pop_front();
dp[j] = dq.front().second + t * v;
}
}
}Binary splitting is shorter: replace copies with bundles and run 0/1 knapsack on the bundles.
Proof: binary splitting represents exactly the counts
Let be maximal with and . Subsets of give every count in (binary representations). Adding the bundle gives every count in , and since these two ranges together cover . No subset exceeds because the total of all bundles is .
Subset sum
bitset<N> dp; dp[0] = 1;
for (int x : a) dp |= dp << x;Bounded total : while some value occurs times, replace two copies by one copy of . Afterwards every value occurs at most twice, so there are items, and the bitset DP costs . When is only known at runtime, dispatch to a templated bitset size (template<int L> void solve() { if (S >= L) return solve<min(2 * L, MAXL)>(); ... }).
Proof: merging duplicates keeps the reachable sums and leaves items
Before the merge there are copies of ; after it, copies of and one . A subset using copies of () is simulated by copies if , or by plus copies otherwise; conversely is two copies of . So the set of reachable sums is unchanged; the total sum is unchanged and the number of items drops, so the process terminates. At the end the distinct values satisfy and , so , , and there are at most items.
Other subset-sum facts:
- Values bounded by : Pisinger’s balancing algorithm runs in .
- “Sum divisible by ” is a residue DP in ; by Erdős–Ginzburg–Ziv, any integers contain whose sum is divisible by .
- “XOR equals ” is linear algebra over (XOR Basis).
- Minimum difference partition: the largest reachable sum .
Counting knapsack and deletions
The counting DP is polynomial multiplication: adding item multiplies the generating function by (0/1) or (unbounded). Both are invertible, so items can be removed in :
void add(vector<mint>& f, int c) { for (int j = sz(f) - 1; j >= c; j--) f[j] += f[j - c]; } // * (1 + x^c)
void remove(vector<mint>& f, int c) { for (int j = c; j < sz(f); j++) f[j] -= f[j - c]; } // / (1 + x^c)
void addUnb(vector<mint>& f, int c) { for (int j = c; j < sz(f); j++) f[j] += f[j - c]; } // / (1 - x^c)
void remUnb(vector<mint>& f, int c) { for (int j = sz(f) - 1; j >= c; j--) f[j] -= f[j - c]; } // * (1 - x^c)
// at most k copies: * (1 - x^{(k+1)c}) / (1 - x^c), two O(W) passesThat answers “for each item, the number of subsets of the others with sum ” in total. Over (or with doubles for probabilities) this is exact; for max/min DPs there is no inverse, so use the tricks below.
Proof: the removal loop divides by
Let , i.e. . Then , where for and is already recovered for larger if we go in increasing . The in-place loop computes exactly this. The unbounded versions are the same identities with (multiplication by ) and its inverse.
Max/min knapsack without deletion
- Range of items, many queries (best value using items with capacity ): divide and conquer on the item array. For a split point , compute knapsack DPs of going left from and of going right, and answer every query crossing by in . Total .
- Queue of items (add newest, remove oldest): two stacks of DP layers (push = one knapsack step from the stack top); a query merges the two tops in for a single capacity.
- Undo (stack order): keep the previous layer per push, or a rollback log.
- Offline add/remove in arbitrary order: segment tree over time; each item is added to nodes; DFS with a stack of layers, .
Many items with the same weight
Items of weight sorted by value descending have concave prefix sums . Within each residue class mod , the update is a max-plus convolution of an arbitrary sequence with the concave , whose argmax is monotone. Divide and conquer gives per residue, per distinct weight. So for weights in the whole problem is independent of .
// b[j] = max_{0 <= j - t < sz(g)} a[t] + g[j - t], g concave
void conv(int jl, int jr, int tl, int tr, const vector<ll>& a, const vector<ll>& g, vector<ll>& b) {
if (jl > jr) return;
int jm = (jl + jr) / 2, best = max(tl, jm - sz(g) + 1); ll bv = LLONG_MIN;
for (int t = best; t <= min(tr, jm); t++) if (a[t] + g[jm - t] > bv) bv = a[t] + g[jm - t], best = t;
b[jm] = bv;
conv(jl, jm - 1, tl, best, a, g, b); conv(jm + 1, jr, best, tr, a, g, b);
}
void addGroup(vector<ll>& dp, int c, vector<ll> vals) { // all items of weight c; unreachable dp = -1e18
sort(all(vals), greater<>()); vector<ll> g{0};
for (ll x : vals) g.push_back(g.back() + x);
int W = sz(dp) - 1;
for (int r = 0; r < c && r <= W; r++) {
vector<ll> a; for (int j = r; j <= W; j += c) a.push_back(dp[j]);
vector<ll> b(sz(a)); conv(0, sz(a) - 1, 0, sz(a) - 1, a, g, b);
for (int t = 0; t < sz(a); t++) dp[r + t * c] = b[t];
}
}Proof: the argmax is monotone
Let (only for ). Take and . The indices satisfy and ; for a concave the outer pair has the smaller sum, . Adding to both sides gives . So if beats at row (), then : it also beats at row . Hence the leftmost argmax is nondecreasing in , and divide and conquer over rows with shrinking column ranges is valid. The band restriction is compatible, because its boundaries and are both nondecreasing in .
Small weights, huge capacity
All , and large. Sort by density descending and let be the longest prefix that fits. Some optimal solution differs from only by a removed set and an added set from the rest, each of weight . So compute “min value lost removing weight exactly from ” and “max value gained adding weight exactly from the rest” for , and combine: .
Proof: the exchange sets are small
Lemma: two sequences of integers in , each of length , have nonempty subsequences with equal sums. Walk with a counter = (sum taken from the first) (sum taken from the second): take from the first if , from the second otherwise. Then always stays in , a set of values; after steps some value repeats, and the items taken between the two occurrences form equal-sum nonempty blocks.
Take an optimal minimising . If , find , of equal weight. Every item of has density every item of (prefix vs suffix of the density order), so , and has the same weight and value , with a smaller symmetric difference; contradiction. So one of them has fewer than items, weight . If : since (the next item didn’t fit) and , . If : we may assume (otherwise any item of fits back in without loss), so .
Related: CF 1132E (weights with counts up to ): take each weight greedily except for at most copies, and DP over the remainder.
Unlimited coins and huge targets
Let be the smallest coin. Amounts with the same residue mod are representable from some threshold on: if is the smallest representable amount , then is representable iff . Compute by Dijkstra on residue vertices with an edge of weight per coin.
vector<ll> residueDist(const vector<int>& coins) { // d[r] = smallest representable x, x = r (mod m)
int m = *min_element(all(coins)); vector<ll> d(m, LLONG_MAX); d[0] = 0;
priority_queue<pair<ll, int>, vector<pair<ll, int>>, greater<>> pq; pq.push({0, 0});
while (sz(pq)) {
auto [dv, r] = pq.top(); pq.pop();
if (dv != d[r]) continue;
for (int c : coins) { int s = (r + c) % m; if (dv + c < d[s]) d[s] = dv + c, pq.push({d[s], s}); }
}
return d;
}
// representable count in [0, X]: sum over r with d[r] <= X of (X - d[r]) / m + 1A heap-free version: for each coin , the edges form cycles; start each cycle at its current minimum and relax around it twice.
Two coprime coins : the largest non-representable amount is and exactly amounts are non-representable.
Proof: residue shortest paths and the two-coin formula
Any multiset of coins with sum , taken in any order, is a walk from residue 0 to residue of length , so . Conversely the shortest walk is a multiset of coins summing to , and adding copies of reaches every . So is representable iff .
For coins coprime and : the residues , , are distinct, and (any representation of an amount in that residue class uses at least copies of ). The largest non-representable amount is . The number of non-representable amounts is (the classical floor-sum identity for coprime ).
Coin change: three different questions
Given denominations, making amount . Three distinct questions, three different recurrences — mixing them up is the most common error in this family:
| Question | Recurrence | Loop order |
|---|---|---|
| is reachable / minimum coins | unbounded knapsack: dp[w] = min(dp[w], dp[w-c]+1) | capacity increasing |
| number of combinations (multiset, order irrelevant) | dp[w] += dp[w-c] | coins outer, amount inner |
| number of permutations (sequences, order matters) | same recurrence body | amount outer, coins inner |
The loop order is the semantics
Identical recurrence bodies, swapped loop order, different answers. Target 3, coins : combinations gives 2 (, ); permutations gives 3 (, , ). This is the same combinations-outer-vs-sequences-outer rule as DP Foundations’ order table.
Greedy (always take the largest coin that fits) is not generally optimal: coins , target 6 — greedy gives (3 coins), optimum is (2 coins). A coin system where greedy is always optimal is called canonical; real currencies happen to be canonical, which is why the wrong intuition persists. Checking whether a given system is canonical is itself nontrivial — Pearson’s algorithm decides it in by checking a bounded set of counterexample candidates. Never assume greedy works unless you’ve verified it for the given denominations.
For two coprime coins, the largest unrepresentable amount and the count of unrepresentable amounts are proved above (Unlimited coins and huge targets); for 3+ coins there’s no closed form and computing the Frobenius number is NP-hard in general, though the same residue-shortest-path technique above still decides representability of one specific (possibly huge) target.
Partitioning problems
Three more DP families that are “knapsack-shaped” but don’t fit the weight/value framing above.
Partition a sequence into contiguous segments (order fixed, choose where to cut):
— best cost splitting the first elements into segments. naively; the same optimisations as elsewhere in this chapter apply directly:
| Condition | Technique | Time |
|---|---|---|
| satisfies the quadrangle inequality | Knuth optimization | |
| is Monge (monotone argmin) | D&C optimization | |
| linear in a parameter | CHT | |
| answer convex in , huge | Aliens trick | |
| segment length bounded | monotone queue |
Minimax partitioning needs no DP at all
“Split into segments minimising the largest segment sum” is not a DP problem: binary search the answer , greedily cut whenever the running sum would exceed , and check the segment count is . — far simpler and faster than any DP formulation, and a common trap for over-applying this section’s table.
Counting integer partitions (write as a sum of positive integers, order irrelevant — the partition function ): this is exactly the coin-change combinations recurrence above with coins , giving in .
Proof: Euler's pentagonal number theorem gives
(Stated, not re-derived — a full proof is a generating-function argument beyond this note’s scope, consistent with how this repo cites rather than re-derives genuinely deep classical theorems elsewhere, e.g. Gordon’s theorem in
01-problem-solving/05.) Euler’s identity gives, after inverting (the partition generating function is the reciprocal of this product), the recurrence summed over with both pentagonal numbers — only terms, each an lookup, giving total for all of .
Variants (small modifications to the same coin-change-style DP, or the generating function): partitions into distinct parts, into odd parts (equinumerous with distinct parts, a classical Euler bijection), into at most parts, with parts of bounded size.
Rod cutting (unbounded knapsack in disguise — cut a rod of length into pieces to maximise total price):
| Variant | Change |
|---|---|
| plain | unbounded knapsack: dp[len] = max(dp[len], dp[len-i]+price[i]) |
| cost per cut | dp[len] = max(price[len], max_i(dp[i]+dp[len-i]-c)) |
| at most pieces | add a dimension, |
| each length usable once | 0/1 knapsack — weight loop downward |
| maximise the product of piece lengths | see below |
| cutting at given positions, cost = piece length | interval DP, see below |
Proof: maximum product of pieces uses only parts 2 and 3
No part is 1: merging a part of with any neighbouring part into a single part keeps the total length fixed and scales the product by — strictly better, so an optimal split has no part equal to . No part exceeds 4: replacing a part by two parts and keeps the length fixed and scales the product by , which exceeds exactly when , i.e. — true for every , so no optimal split uses a part . No part is 4: a part of splits into two parts of with the same product () and the same length, so we may always rewrite it that way without loss. At most two parts equal 2: three parts of sum to the same length as two parts of (), but , so replacing any three s by two s strictly improves the product — hence an optimal split has at most two s. Combining these, every optimal split uses only s and s with at most two s; writing , , that forces all s, two s and threes (since is handled as , not a bare leftover ), one and threes. ( is the continuous intuition for why dominates — the continuous optimum is , nearest integer .)
Cutting sticks (a stick of length must be cut at given positions, each cut costs the length of the piece being cut, minimise total cost):
with the cut positions (plus and ) as the DP’s endpoints. naively, with Knuth optimization (the cost satisfies the quadrangle inequality). This is the same recurrence as matrix chain multiplication and merging-stones problems — interval DP is one pattern wearing several costumes.
Reconstruction
- Keep
take[i]as abitset<W+1>per item ( bits), then walk back: iftake[i][w], use item and set . - Or keep the full table and compare
dp[i+1][w] != dp[i][w]. - Subset sum with a 1D array: record
from[s]= item that first made reachable; walking back throughfromuses each item at most once because sums become reachable in item order.