~/dynamic-programming/knapsack
0/1 knapsack
Pick items with the most total value that fit under a weight limit, each item at most once, by filling a table of item × capacity.
dp[i][c] is the best value using the first i items with room c. Each cell is the better of skipping item i or taking it.
You choose a subset of things, each usable once, under a budget or weight limit, and the limit is a small-ish integer.
O(n·W)
O(n·W), or O(W) with one row
You’ll recognise it when
- You pick a subset of items, and each item can be used at most once.
- There’s a hard limit on the total: weight, cost, time, a target sum.
- You maximise a value, or ask whether a total is reachable, or count how many subsets reach it.
- The limit is an integer small enough to index an array with, say up to 10⁵.
If each item can be reused as often as you like (coins, for example), it’s the unbounded knapsack instead. The table is the same idea with one change, see Variations.
The idea
You’re packing a bag that holds 7 kg. Each item has a weight and a value, and you want the most value that fits. Trying every subset works for 4 items (16 subsets) but not for 40 (a trillion).
Instead, look at the items one at a time and answer a smaller question first: with only the first i items and room c, what’s the best value? For item i there are just two choices. Skip it, and the answer is whatever the first i - 1 items managed with the same room. Take it, and you gain its value but have w less room for the earlier items, whose best for that smaller room you’ve already worked out. Keep the better of the two.
How it works
Build a table with one row per item (plus a row 0 for “no items”) and one column per capacity from 0 to W. dp[i][c] is the best value using only the first i items with room c. The graphic fills it row by row as you scroll.
- Row 0 is all zeros. With no items to choose from, every capacity is worth nothing.
- Too heavy: copy from above. When
c < w, itemican’t fit, so skipping is the only option:dp[i][c] = dp[i-1][c]. - It fits: two candidates. Skip it and keep
dp[i-1][c], the cell straight above. Or take it: the best for the room left over,dp[i-1][c-w], plus its value. That cell is one row up andwcolumns left. - Taking wins, so write the larger number. Here item 1 at
c = 4: 0 + 5 beats 0. - Skipping wins. At
c = 4, item 2 gives only 0 + 4, while the cell above already holds 5 from item 1. Taking an item isn’t free: it uses room the earlier items might use better. - The best value-per-kilo item is skipped. Item 3 has the best ratio (7 / 5), yet at
c = 7taking it leaves 2 kg, worth 0, for a total of 7. Items 1 and 2 together make 9. - Read the answer, then walk back. The answer is the bottom-right cell. If a cell equals the one above it, item
iwasn’t needed: move up. - A cell that differs was taken. Record item
i, then jump up andwcolumns left, to the room that was left before it went in. - Done: items 1 and 2, weight 7, value 9.
Why it’s correct: every subset of the first i items either leaves item i out or puts it in. The best subset that leaves it out is dp[i-1][c]; the best that puts it in is item i plus the best subset of the others that fits in c - w, which is dp[i-1][c-w]. Both cells are in the row above, already final, so every cell is right once the row above is.
def knapsack(items, capacity):
"""items is a list of (weight, value). Returns (best value, chosen item indices)."""
n = len(items)
# dp[i][c] = best value using only the first i items, with capacity c
dp = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
w, v = items[i - 1]
for c in range(capacity + 1):
dp[i][c] = dp[i - 1][c]
if w <= c and dp[i - 1][c - w] + v > dp[i][c]:
dp[i][c] = dp[i - 1][c - w] + v
# Walk back from the bottom-right corner to see which items were taken.
chosen, c = [], capacity
for i in range(n, 0, -1):
if dp[i][c] != dp[i - 1][c]:
chosen.append(i - 1)
c -= items[i - 1][0]
chosen.reverse()
return dp[n][capacity], chosen#include <algorithm>
#include <utility>
#include <vector>
using namespace std;
// items holds {weight, value}. Returns {best value, chosen item indices}.
pair<long long, vector<int>> knapsack(const vector<pair<int, long long>>& items, int capacity) {
int n = items.size();
// dp[i][c] = best value using only the first i items, with capacity c
vector<vector<long long>> dp(n + 1, vector<long long>(capacity + 1, 0));
for (int i = 1; i <= n; i++) {
auto [w, v] = items[i - 1];
for (int c = 0; c <= capacity; c++) {
dp[i][c] = dp[i - 1][c];
if (w <= c && dp[i - 1][c - w] + v > dp[i][c]) {
dp[i][c] = dp[i - 1][c - w] + v;
}
}
}
// Walk back from the bottom-right corner to see which items were taken.
vector<int> chosen;
int c = capacity;
for (int i = n; i >= 1; i--) {
if (dp[i][c] != dp[i - 1][c]) {
chosen.push_back(i - 1);
c -= items[i - 1].first;
}
}
reverse(chosen.begin(), chosen.end());
return {dp[n][capacity], chosen};
}import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
class Knapsack {
// Returns the best value; the indices of the chosen items go into `chosen`.
static long solve(int[] weight, long[] value, int capacity, List<Integer> chosen) {
int n = weight.length;
// dp[i][c] = best value using only the first i items, with capacity c
long[][] dp = new long[n + 1][capacity + 1];
for (int i = 1; i <= n; i++) {
int w = weight[i - 1];
long v = value[i - 1];
for (int c = 0; c <= capacity; c++) {
dp[i][c] = dp[i - 1][c];
if (w <= c && dp[i - 1][c - w] + v > dp[i][c]) {
dp[i][c] = dp[i - 1][c - w] + v;
}
}
}
// Walk back from the bottom-right corner to see which items were taken.
int c = capacity;
for (int i = n; i >= 1; i--) {
if (dp[i][c] != dp[i - 1][c]) {
chosen.add(i - 1);
c -= weight[i - 1];
}
}
Collections.reverse(chosen);
return dp[n][capacity];
}
}The code looks only at row i - 1 while filling row i, so one array of size W + 1 is enough. The catch: loop c downward. Going down, dp[c - w] hasn’t been touched for this item yet, so it still means “without item i”. Going up, it may already include item i, and the item gets taken twice.
dp = [0] * (W + 1)
for w, v in items:
for c in range(W, w - 1, -1): # downward: each item at most once
dp[c] = max(dp[c], dp[c - w] + v)
You lose the traceback with one row, so keep the full table when you need the chosen items.
Why it’s O(n·W)
There are (n + 1) × (W + 1) cells and each takes one comparison, so time is O(n·W). The walk back is O(n). The table takes O(n·W) memory; the one-row version takes O(W).
That looks polynomial but isn’t quite. W is a number in the input, written with about log₂ W digits, and the running time grows with its value. Add ten bits to W and the table gets 1,000 times wider. That’s called pseudo-polynomial. Knapsack is NP-hard, and this doesn’t contradict that: with W = 10¹² the table is hopeless.
| n | W | cells |
|---|---|---|
| 100 | 10,000 | 10⁶ |
| 1,000 | 10⁵ | 10⁸ |
| 40 | 10¹² | too many: try meet in the middle |
Common mistakes
Greedy by value per weight
Taking the best ratio first feels right and is wrong. In the example, item 3 has the best ratio (1.4). Greedy takes it, then only item 4 still fits: value 8. The table finds items 1 and 2: value 9. Greedy only works for the fractional knapsack, where you may take part of an item.
Looping upward in the one-row version
Going up, dp[c - w] may already contain the current item, so it gets packed again and again. That’s the unbounded knapsack, not 0/1.
for c in range(w, W + 1): # ✗ reuses the item
for c in range(W, w - 1, -1): # ✓ each item at most once
Skipping the too-heavy columns in the 2-D table
If the inner loop starts at c = w, the cells with c < w in row i stay 0 instead of copying the row above, and later rows read those zeros.
for c in range(w, W + 1): ... # ✗ dp[i][c] for c < w stays 0
for c in range(W + 1): # ✓ copy first, then try taking
dp[i][c] = dp[i - 1][c]
Overflowing the total value
With 100 items worth up to 10⁹ each, the best value is up to 10¹¹. That overflows a 32-bit int in C++ and Java; use long long or long for the table.
Variations
- Unbounded knapsack and coin change. Each item may be used any number of times. Take from the same row:
dp[i][c-w] + v. In one row, loopcupward. Minimum coins for an amount usesminand a start of 0 atdp[0]. - Subset sum and equal partition. Values don’t matter, only whether a total is reachable:
dp[c] = dp[c] or dp[c - w]. To split an array into two equal halves, check whethertotal / 2is reachable (and give up if the total is odd). - Counting subsets. Replace
maxwith+:dp[c] += dp[c - w], starting fromdp[0] = 1. Target Sum reduces to this. - One-row space optimisation. O(W) memory, loop capacity downward, as shown after the code. Keep the table if you need to list the chosen items.
- Big capacity, small values. Swap the roles:
dp[v]is the smallest weight that achieves valuev, then take the largestvwhose weight fits. That’s O(n · total value).
Check yourself
5 quick questions. Pick an answer to see why it's right or wrong.
-
1
Can an array of positive integers be split into two groups with equal sums? What’s the standard approach?
If one group sums to
total / 2, the rest is the other half, so it’s a reachability question where each number is used at most once. The greedy “lighter group” idea fails on[3, 3, 2, 2, 2]: it can end 7 vs 5 although 6 + 6 exists. Unbounded knapsack would let one number count twice. -
2
This is meant to be 0/1 knapsack (each item at most once). What does it print?
C = 6items = [(2, 3)] # (weight, value)dp = [0] * (C + 1)for w, v in items:for c in range(w, C + 1):dp[c] = max(dp[c], dp[c - w] + v)print(dp[C])Going upward,
dp[4]builds ondp[2], which already holds the item, anddp[6]builds ondp[4]: the single item is packed three times. Loopingcdownward gives the correct 0/1 answer, 3. -
3
best([(1, 5), (3, 1)], 2)should be 5 (take the first item), but this returns 0. Why?def best(items, C):dp = [[0] * (C + 1) for _ in range(len(items) + 1)]for i, (w, v) in enumerate(items, 1):for c in range(w, C + 1):dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - w] + v)return dp[-1][C]The second item weighs 3, so its row never touches columns 0 to 2, and
dp[2][2]stays 0 even thoughdp[1][2]is 5. Loop over everycand copy from above first. Direction doesn’t matter in the 2-D table because rowionly reads rowi - 1; readingdp[i][c - w]would turn it into the unbounded knapsack. -
4
The knapsack DP runs in O(n·W). Why is that called pseudo-polynomial?
The input size counts the digits of
W, about log W of them, and W = 2^(log W). A capacity of 10^12 is 13 characters of input but a table far too wide to fill. Knapsack is NP-hard, and this DP doesn’t contradict that. (Values can be any numbers; it’s the weights that index the table.) -
5
Book Shop:
nbooks with a price and a page count, a budgetx; maximise pages, each book at most once. How much memory does the value alone need?Row
iof the table only reads rowi - 1, so one array updated downward is enough. You need the whole O(n·x) table only to list which books were chosen. Time is O(n·x) either way.