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

VariantRecurrence / methodCost
0/1for w = W..c: dp[w] = max(dp[w], dp[w-c] + v)
unboundedsame, 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 boundedmerge duplicates: items
counting subsetsdp[w] += dp[w-c]; reversible (deletions)
many items with equal weightconcave 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 coinsshortest paths modulo the smallest coin
items on a tree (take parent to take child)tree knapsack (Tree DP) or
fractionalgreedy by density
-approximationround 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 knapsacksdp[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.

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.

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)>(); ... }).

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) passes

That 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.

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];
    }
}

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: .

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 + 1

A 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.

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:

QuestionRecurrenceLoop order
is reachable / minimum coinsunbounded 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 bodyamount 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:

ConditionTechniqueTime
satisfies the quadrangle inequalityKnuth optimization
is Monge (monotone argmin)D&C optimization
linear in a parameterCHT
answer convex in , hugeAliens trick
segment length boundedmonotone 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 .

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):

VariantChange
plainunbounded knapsack: dp[len] = max(dp[len], dp[len-i]+price[i])
cost per cutdp[len] = max(price[len], max_i(dp[i]+dp[len-i]-c))
at most piecesadd a dimension,
each length usable once0/1 knapsack — weight loop downward
maximise the product of piece lengthssee below
cutting at given positions, cost = piece lengthinterval DP, see below

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 a bitset<W+1> per item ( bits), then walk back: if take[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 through from uses each item at most once because sums become reachable in item order.