Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Coding · Dynamic programming

The 0/1 knapsack

Lesson · 13 min read · Dynamic programming · 0/1 knapsack

Each item is taken once or not at all. The state is (items considered, capacity left), and each item is a take-or-skip decision.

The problem

Items have weights and values; a bag holds at most a given weight. Choose a set of items, each at most once, with the largest total value that fits. With items of weight 1, 3, 4, 5 kg worth 1, 4, 5 and 7 points and a 7 kg bag, the best is the 3 kg and 4 kg items, 9 points. Budget allocation, choosing projects under a headcount limit, and loading cargo are all knapsacks.

Greedy by value per kilogram fails: it takes the 5 kg item (1.4 points per kg) first and is left with 2 kg, worth 1 point at most, 8 in all.

State and recurrence

Decide the items one at a time. After deciding the first i items, what matters for the rest is only how much capacity is left. So the state is (i, c): the best value using items 1..i with capacity c.

Recurrence

best(i, c)

  1. skip item i: best(i − 1, c);
  2. take it, if it fits: value[i] + best(i − 1, c − weight[i]);
  3. take the larger.

base cases

  1. best(0, c) = 0: no items, no value.

Taking item i moves to row i − 1: that is what "at most once" means in the recurrence.

Bottom-up

Rows add one item at a time; columns are capacities 0 to 7. Each cell reads the row above: straight up (skip) or up and to the left by the item's weight (take).

  1. Step 1 / 5
    01234567none000000001kg·13kg·44kg·55kg·7

    Base cases filled first.

  2. Step 2 / 5
    01234567none000000001kg·1011111113kg·44kg·55kg·7

    Row 1kg·1 filled.

  3. Step 3 / 5
    01234567none000000001kg·1011111113kg·4011455554kg·55kg·7

    Row 3kg·4 filled.

  4. Step 4 / 5
    01234567none000000001kg·1011111113kg·4011455554kg·5011456695kg·7

    Row 4kg·5 filled.

  5. Step 5 / 5
    01234567none000000001kg·1011111113kg·4011455554kg·5011456695kg·701145789

    Row 5kg·7 filled. best(4, 7) = 9: the 3 kg and 4 kg items.

One row is enough

Row i reads only row i − 1, at the same capacity or a smaller one. So keep a single array and update it in place with capacities going downwards: when best[c] is updated, best[c − w] still holds the previous row's value, because smaller capacities have not been touched yet in this pass.

def knapsack(weights, values, cap):
    best = [0] * (cap + 1)
    for w, v in zip(weights, values):
        for c in range(cap, w - 1, -1):        # downwards: each item used at most once
            best[c] = max(best[c], v + best[c - w])
    return best[cap]

Going upwards instead would let best[c − w] already include item i, so the item could be taken again and again. That bug is the unbounded knapsack, and the next-but-one module uses it on purpose.

Implementation

In six languages:

def knapsack_01(weights, values, capacity):
    best = [0] * (capacity + 1)                # best[c]: most value within capacity c
    for w, v in zip(weights, values):
        for c in range(capacity, w - 1, -1):   # downwards: each item at most once
            best[c] = max(best[c], v + best[c - w])
    return best[capacity]

The same code is the reference solution to The 0/1 knapsack, where it is compiled and run against the tests in all six languages.

TimeO(n · capacity)
SpaceO(capacity)

Pseudo-polynomial: the running time grows with the numerical size of the capacity, not just the number of items. With capacities in the billions, use a different state, such as the minimum weight for each total value.