1. Introduction and Motivation
0/1 Knapsack and Unbounded Knapsack cover the two extremes of item availability: take an item once, or take it as many times as capacity allows. Coin Change covers the two questions usually asked of an unbounded knapsack: what is the minimum count needed, and how many ways exist to hit a target. Real problems rarely sit at either extreme. A warehouse has a limited stock of each SKU, not one unit and not infinite units. A vending machine has a bounded number of each coin, not a mint. A logistics run has several trucks, not one bag. And some counting questions care about the order in which choices happen, not just which choices happen.
This lesson covers exactly the variants that fill those gaps: Bounded Knapsack, Multiple Knapsack, Subset Sum, Partition Equal Subset Sum, Ordered Compositions, and Bounded Coins. Each one reuses a DP skeleton you already know — the difference is a changed state, a reordered loop, or a reduction trick (binary grouping) that turns a hard bounded problem into an easy 0/1 problem.
2. Prerequisites
This lesson assumes you have already worked through 0/1 Knapsack and Unbounded Knapsack and both Coin Change variants — minimum coins and number of ways — in Part 15: Core DP Patterns. Specifically, you should already be comfortable with:
- Why 0/1 Knapsack traverses capacity backwards and Unbounded Knapsack traverses it forwards.
- The
coin_changeminimum-coins recurrence, $dp[a] = \min(dp[a-coin]+1)$. - The
coin_change_waysrecurrence with coins in the outer loop.
None of that is re-derived here. Every section below either extends one of those four algorithms with a new constraint, or contrasts it with a variant that changes exactly one design decision.
3. Bounded Knapsack
Why grouping works: choosing a subset of the super-items for a given original item is equivalent to choosing how many physical copies of that item to take. The greedy split — take $\min(k, \text{remaining})$ copies where $k$ doubles each round — guarantees every count from $0$ to $c_i$ is reachable as the sum of some subset of the resulting group sizes. Once every item is replaced by its super-items, each super-item behaves like an ordinary 0/1 item: a weight of (group size × unit weight) and a value of (group size × unit value).
| Count $c_i$ | Groups produced | Super-items |
|---|---|---|
| 3 | 1, 2 | 2 |
| 5 | 1, 2, 2 | 3 |
| 6 | 1, 2, 3 | 3 |
| 7 | 1, 2, 4 | 3 |
Notice count 5 does not decompose as the "textbook" $1, 2, 2^{k-1}$ powers-of-two-only scheme; the last group is whatever remains once the running total would otherwise exceed $c_i$. That is exactly what keeps the algorithm correct for any count, not just counts that are one less than a power of two.
Multiple Knapsack
Multiple Knapsack generalizes further: instead of one bag, there are several bins, each with its own capacity, and every item may go into at most one bin (or be left out). Deciding the optimal assignment across many bins is NP-hard in general — it combines bin packing with knapsack selection. For a small, fixed number of bins, an exact DP is still practical: extend the state to include the remaining capacity of every bin, not just one.
For two bins the state is (item index, remaining capacity of bin 1, remaining capacity of bin 2); each item is either skipped, placed in bin 1 (if it fits), or placed in bin 2 (if it fits). A third bin adds one more capacity dimension to the state — the recursion is identical, only the branching factor and state size grow. Beyond two or three bins and small capacities, this state space explodes, and production systems fall back to greedy-plus-local-search heuristics or an ILP solver instead of exact DP.
4. Subset Sum
Trace subset_sum([6, 2, 5], 7)
Track the set of sums reachable after each item is considered, instead of a full boolean grid:
| Items processed | Reachable sums |
|---|---|
| none | {0} |
| 6 | {0, 6} |
| 6, 2 | {0, 2, 6, 8} |
| 6, 2, 5 | {0, 2, 5, 6, 7, 8, 11, 13} |
Target 7 appears once all three items are considered — reached by $2 + 5$ — so subset_sum([6, 2, 5], 7) is true. Each row is exactly one pass of the $dp[i][\cdot]$ recurrence: every new item either leaves a sum unreached, or adds (sum + item) to the reachable set.
Partition Equal Subset Sum
Partition Equal Subset Sum asks whether an array can be split into two subsets with equal totals — which is exactly Subset Sum with target = total_sum // 2 (and an immediate "no" if the total is odd).
5. Ordered Compositions
coin_change_ways puts coins in the outer loop and amount in the inner loop — each coin type is fully "locked in" before the next is considered, so $1+2$ and $2+1$ collapse into the same combination. Swap the loop order — amount in the outer loop, coins in the inner loop — and every amount is rebuilt from scratch by trying every coin, so $1+2$ and $2+1$ are counted as two different ordered compositions.
| Loop order | Step | dp array after step |
|---|---|---|
| Combinations (coin outer) | start | [1, 0, 0, 0] |
| process coin 1 | [1, 1, 1, 1] | |
| process coin 2 | [1, 1, 2, 2] | |
| Compositions (amount outer) | dp[0] | [1, 0, 0, 0] |
| dp[1] (coins 1, 2) | [1, 1, 0, 0] | |
| dp[2] (coins 1, 2) | [1, 1, 2, 0] | |
| dp[3] (coins 1, 2) | [1, 1, 2, 3] |
In the combinations pass, coin 1 finishes populating dp[1..3] before coin 2 is ever considered, so a coin-2-then-coin-1 arrangement is never separately counted. In the compositions pass, dp[3] is built by asking "what was the count for $3-1=2$?" and "what was the count for $3-2=1$?" independently, at a point where both already include every arrangement that used any mix of coins — so $(1,2)$ and $(2,1)$ both get added in.
Bounded Coins
Bounded Coins is Coin Change with a limited supply per denomination — denomination $i$ can be used at most counts[i] times when making the amount. This is the same shape of problem as Bounded Knapsack, so the same binary-grouping trick applies directly: split each denomination's supply into $O(\log \text{counts}[i])$ super-coins, then run a 0/1-style minimum-coins DP over all super-coins, tracking the number of physical coins each super-coin represents as its cost.
6. A Note on Bitset Optimization
reachable |= reachable << weight, which updates every reachable sum in parallel — a machine word processes dozens or hundreds of sums per shift-and-OR instead of one sum per loop iteration. This is a genuine asymptotic win in practice (a constant-factor speedup equal to the machine word size), not just a micro-optimization. The general technique, bitset DP, extends well beyond Subset Sum to other DPs whose transitions are unions or shifts over a bounded domain. It is covered in full, with implementation and complexity analysis, in the sibling lesson Bitset DP. Nothing here depends on it; it is worth knowing it exists before you reach for a plain boolean array on a large target.
7. Complexity Comparison Table
| Variant | State Definition | Time | Space |
|---|---|---|---|
| Bounded Knapsack | $dp[w]$ over binary-grouped super-items | $O(n \cdot W \cdot \log(\max c_i))$ | $O(W)$ |
| Multiple Knapsack (small, fixed bins) | $dp[i][r_1][r_2]\dots$, remaining capacity per bin | $O(n \cdot \prod_b (\text{cap}_b + 1))$ | same as time (memo table) |
| Subset Sum | $dp[i][s]$ = first $i$ items reach sum $s$ | $O(n \cdot \text{target})$ | $O(n \cdot \text{target})$, or $O(\text{target})$ 1D |
| Partition Equal Subset Sum | Subset Sum with target $= \text{sum}/2$ | $O(n \cdot \text{sum})$ | $O(\text{sum})$ |
| Ordered Compositions | $dp[a]$, amount in the outer loop | $O(\text{amount} \cdot |\text{coins}|)$ | $O(\text{amount})$ |
| Bounded Coins | $dp[s]$ over binary-grouped super-coins | $O(|\text{coins}| \cdot \text{amount} \cdot \log(\max \text{counts}))$ | $O(\text{amount})$ |
8. Applications and Alternatives
Where each variant shows up in practice
- Bounded Knapsack: cargo or cart selection with limited per-SKU inventory, where items are not one-off and not infinite.
- Multiple Knapsack: loading a small, fixed fleet of trucks or a handful of shipping containers; beyond a few bins, exact DP gives way to heuristics or ILP solvers.
- Subset Sum: checking exact-change feasibility, matching tax lots or invoice line items to a target total, and the historical (now broken) knapsack-based public-key cryptosystems.
- Partition Equal Subset Sum: splitting a workload or a set of tasks between two workers as evenly as possible.
- Ordered Compositions: counting the number of distinct move sequences (e.g. weighted staircase steps) that reach a target, where sequencing itself is part of the answer.
- Bounded Coins: vending machines and cash registers with a finite till, where minimum change must respect actual coin stock.
All six are DP at heart, but the alternative worth knowing is when DP itself is the wrong tool: for Subset Sum and Partition with very large sums but few items, a meet-in-the-middle split (enumerate half the items, sort, binary search the other half) can beat $O(n \cdot \text{sum})$ when sum dominates $2^{n/2}$. For Multiple Knapsack at real fleet scale, a mixed-integer program or a metaheuristic replaces exact DP entirely.
9. Exercises and Mastery
Easy
- Trace
subset_sum([1, 5, 11, 5], 10)using the reachable-sums table from Section 4, and confirm the answer is true. - State why
subset_sum(nums, 0)is always true regardless ofnums.
Medium
- Partition to K Equal Sum Subsets generalizes Partition Equal Subset Sum from 2 buckets to $k$ buckets, each required to sum to $\text{total}/k$. Design (code optional) an approach: a bitmask DP over which items have been used ($dp[\text{mask}]$ = remainder in the bucket currently being filled) is one option; backtracking with pruning (sort descending, skip duplicate bucket-fill attempts) is another. Explain why the plain Subset Sum reduction that works for $k=2$ does not extend cleanly to $k=3$ or more.
- Explain why Ordered Compositions' state, $dp[a]$, does not need to track which coins were already used, while a naive "count distinct sequences" approach might tempt you to track a used-set.
Hard
- For items with weights $[5, 7, 3]$, values $[10, 14, 7]$, and counts $[100, 50, 200]$, work out how many super-items binary grouping produces for each (the grouping loop from Section 3 gives 7, 6, and 8 super-items respectively), then explain why solving 0/1 Knapsack on 21 super-items is dramatically cheaper than the naive $O(n \cdot W \cdot \max c_i)$ approach when $W$ is in the thousands.
- Prove that binary grouping's
take = min(k, remaining)loop always makes every count from $0$ to $c_i$ reachable as a subset sum of the produced groups, even when the last group is smaller than the doubling sequence would predict.
Review rhythm: tomorrow, reconstruct the binary-grouping loop and the combinations/compositions loop-order swap from memory. Next week, implement all six variants in a language you did not use here. Several weeks later, recognize a "limited supply" or "order matters" constraint in a new problem and immediately name which of these six patterns applies, without searching for it.
10. Summary Cheat Sheet
- Bounded Knapsack: binary-group each item into $O(\log c_i)$ super-items, then run ordinary 0/1 Knapsack.
- Multiple Knapsack: extend the DP state with one remaining-capacity dimension per bin; exact only for a small, fixed number of bins.
- Subset Sum: $dp[i][s]$ tracks reachability, not value — a yes/no DP, not a max/min DP.
- Partition Equal Subset Sum: Subset Sum with target $= \text{sum}/2$; nothing new to derive.
- Ordered Compositions: swapping the coin/amount loop order turns "ways" (Part 15) into "ordered ways" — no new state, just a different iteration order.
- Bounded Coins: the same binary-grouping trick as Bounded Knapsack, applied to a minimum-coins objective instead of a maximum-value objective.
- Binary grouping preserves optimization answers (min/max) exactly, but it is not safe to reuse for counting the number of ways — it can create multiple representations of the same physical count.
- Bitset DP accelerates Subset Sum's inner loop by packing reachable sums into machine words; see Bitset DP for the full technique.