Python Program for the 0/1 Knapsack Problem
Advanced Dynamic programming Time O(n × W) Space O(n × W)
Finds the most value that fits in a bag with a weight limit, taking each item at most once, by filling in a table.
In the 0/1 knapsack problem you have a bag that holds a limited weight and some items, each with a weight and a value. Which items should go in to carry the most value? Each item goes in whole or not at all, which is what the 0/1 means. With a bag that holds 7 and items weighing 1, 3, 4 and 5, worth 1, 4, 5 and 7, the best is the items weighing 3 and 4, worth 9. Taking the most valuable item first, the one weighing 5, gets only 8, as only the item weighing 1 fits beside it. Trying every mix works for 4 items (16 mixes) but not for 50 (2⁵⁰, over a thousand trillion), so the program fills in a table of smaller problems instead: the best value for the first few items and a smaller bag.
Program
def knapsack(weights, values, capacity):
"""The most value that fits, taking each item at most once."""
n = len(weights)
# best[i][w]: the most value from the first i items in weight w
best = [[0] * (capacity + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
weight, value = weights[i - 1], values[i - 1]
for w in range(capacity + 1):
best[i][w] = best[i - 1][w] # without item i
if weight <= w: # with item i, if it fits
with_item = best[i - 1][w - weight] + value
best[i][w] = max(best[i][w], with_item)
return best[n][capacity]
weights = [1, 3, 4, 5]
values = [1, 4, 5, 7]
print("Best value for capacity 7:", knapsack(weights, values, 7))
Open in the editor Download knapsack.py
Output
Best value for capacity 7: 9
How it works
Step through the program line by line: each step below shows up as its line runs.
-
nis the number of items, andbestis a table with a row for each number of items considered, 0 to 4, and a column for each capacity, 0 to 7.best[i][w]will be the most value the firstiitems can give in a bag that holdsw. Row 0, with no items, stays all 0. - The rows are filled one at a time, each adding one item.
weightandvalueare that item's (the lists count from 0, the rows from 1). - For each capacity
w, one choice is to leave the item out: then the best is what the items before it managed with the same capacity, the cell above. - The other choice, if the item fits, is to take it: its value, plus the best the items
before it can do in the weight left over,
w - weight. The cell keeps the larger of the two. - The bottom-right cell, with every item and the full capacity, is the answer.
- The program packs the four items into a bag that holds 7.
The table, one row per item added:
| Items | w = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| none | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| + weight 1, value 1 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| + weight 3, value 4 | 0 | 1 | 1 | 4 | 5 | 5 | 5 | 5 |
| + weight 4, value 5 | 0 | 1 | 1 | 4 | 5 | 6 | 6 | 9 |
| + weight 5, value 7 | 0 | 1 | 1 | 4 | 5 | 7 | 8 | 9 |
Take the 9 in the row for the weight-4 item: leaving it out gives 5, the cell above. Taking it leaves room for 3, where the items before it give 4, so 4 + 5 = 9. In the last row, taking the weight-5 item gives 7 plus the 1 that fits in the 2 left over, 8, which is less than the 9 above, so the 9 stays.
Time and space complexity
| Time | O(n × W) |
|---|---|
| Extra space | O(n × W) |
The table has (n + 1) × (W + 1) cells for n items and a capacity of W, and each cell takes a fixed amount of work, so the time and the space are both O(n × W). That's fast while W is small, but it grows with the capacity's size, not with how many digits it has: a capacity of a million needs a million columns. Each row reads only the row above it, so one row is enough when only the best value is wanted (below): O(W) space.
Other ways to write it
With one row
One list holds the current row, updated in place. Going through the capacities from high to low means
best[w - weight] still holds the value from before this item, so each item is taken at most once.
def knapsack(weights, values, capacity):
best = [0] * (capacity + 1)
for weight, value in zip(weights, values):
for w in range(capacity, weight - 1, -1):
best[w] = max(best[w], best[w - weight] + value)
return best[capacity]
print(knapsack([1, 3, 4, 5], [1, 4, 5, 7], 7))With recursion and lru_cache
Top-down: the best from item i onwards with room left is the better of skipping that item and taking it.
lru_cache, a decorator, remembers the answer for each (i, room), so each is worked out once.
from functools import lru_cache
def knapsack(weights, values, capacity):
@lru_cache(maxsize=None)
def best(i, room):
if i == len(weights):
return 0
skip = best(i + 1, room)
if weights[i] > room:
return skip
return max(skip, values[i] + best(i + 1, room - weights[i]))
return best(0, capacity)
print(knapsack([1, 3, 4, 5], [1, 4, 5, 7], 7))Try it yourself
- Change the capacity to 5, then to 9. The best values are 7 and 12.
- Find which items to take: start at the bottom-right cell and go up a row at a time. Where a cell differs from the
one above, that row's item was taken, so take its weight off
w. For a capacity of 7 that's the items weighing 4 and 3. - Print
besta row at a time before the function returns, and compare it with the table above. - Let each item be taken any number of times (the unbounded knapsack): in the one-row version, go through the capacities from low to high. With a capacity of 10 it takes the weight-5 item twice, for 14, where the 0/1 answer is 13.