Algorithm

0/1 Knapsack

Dynamic Programming Pattern

0/1 Knapsack

You are given n items, where item i has a positive weight weights[i] and a positive value values[i], along with a bag that can carry a total weight of at most W. Each item may be taken at most once — you cannot take a fraction of an item and you cannot take the same item twice. Return the maximum total value you can carry without the total weight exceeding W.

CONSTRAINTS
  • 1 ≤ n ≤ 1000
  • 1 ≤ W ≤ 1000
  • 1 ≤ weights[i], values[i] ≤ 1000
  • Each item is taken either zero times or one time; no fractions
EXAMPLE 1
Input: W = 10, weights = [6, 5, 5], values = [30, 24, 24]
Output: 48
Taking the two 5-weight items uses the full capacity for 48. The single 6-weight item is worth more per unit of weight, but taking it wastes 4 units of capacity that nothing can fill.
EXAMPLE 2
Input: W = 7, weights = [1, 3, 4, 5], values = [1, 4, 5, 7]
Output: 9
Items of weight 3 and 4 total exactly 7 units and give 4 + 5 = 9. The weight-5 item plus the weight-1 item also fits, but yields only 8.
EXAMPLE 3
Input: W = 4, weights = [5, 6], values = [100, 200]
Output: 0
Nothing fits in the bag, so nothing is taken. An empty bag is a legal answer and its value is zero, not an error.
EXAMPLE 4
Input: W = 8, weights = [4, 4], values = [3, 3]
Output: 6
Both items fit exactly, so both are taken. Two copies of the same weight and value are still two separate items.
EXAMPLE 5
Input: W = 10, weights = [4], values = [9]
Output: 9
The single item is taken once and 6 units of capacity go unused. There is no penalty for leaving room, and the item cannot be taken a second time to fill it.
Can I take an item more than once, or take a fraction of one?
Neither — that is what 0/1 means. Both relaxations are genuinely different problems: unlimited copies gives the unbounded knapsack, which is the same table with an ascending capacity sweep, and fractions make the problem greedily solvable by value-to-weight ratio.
Do I have to fill the bag completely?
No. Leftover capacity is free. This is why the base case returns zero for every remaining capacity rather than penalising unused room.
How large can the capacity be?
This is the question to ask first, because the running time is proportional to it. A capacity in the thousands makes the table approach correct; a capacity in the billions means the table has too many columns to build, and the intended solution is something else.
Can weights or values be zero or negative?
Here both are positive. A zero-weight item with positive value should simply always be taken. Negative values would never be worth taking, and negative weights would break the whole model, since capacity could grow — worth flagging if the statement is vague about it.
Should I return the chosen items or just the total value?
Just the value. To recover the items you keep the full two-dimensional table and walk backwards from dp[0][W], checking at each row whether the value came from the skip cell or the take cell — if it differs from dp[i+1][c], item i was taken.
The problem in one sentence

You have a bag with a weight limit and a pile of items, each with a weight and a value. Pick a set of items whose total weight fits, and make their total value as large as possible. The "0/1" in the name means each item is all-or-nothing: you take it whole, or not at all.

That last restriction is the whole difficulty. If you were allowed to take fractions of items, this problem would be easy and would not need a table at all — you would sort the items by value-per-kilogram, greedily fill up with the best ratio first, and slice the last item to fit exactly. That works because with fractions there is never any wasted space.

Why the ratio rule breaks when items are indivisible

Take W = 10 with three items: (weight 6, value 30), (weight 5, value 24) and (weight 5, value 24).

Ratios: the first item is 5.0 per kilo, the other two are 4.8 per kilo. So the greedy rule grabs the first item, leaving 4 units of space, which nothing else fits into. Total value: 30.

But taking the two lesser items uses all 10 units and yields 48.

The failure is not bad luck, it is structural: taking the best-ratio item can leave behind a gap that nothing fills. With indivisible items, how well things fit together matters as much as how good each one is on its own, and no per-item ranking can see that. Once a greedy order is provably wrong, the only honest option is to compare the futures of both choices. That is where take-or-skip comes in.

The state, as a story

I go through the items one at a time, from the last toward the first. Standing at item i with c units of room left in the bag, I have two choices:

- Take item i (only if weights[i] <= c) — I gain values[i] and lose that much room: values[i] + f(i-1, c - weights[i]).
- Skip item i — I gain nothing and my room is untouched: f(i-1, c).

I keep the better of the two. The two things I must track are which item I'm on (i) and how much room is left (c) — the bag doesn't care which items are already inside, only how full it is.

State Definitionf(i, c) is the most value I can get using items 0 through i with c units of capacity available.

f(i, c) = max( f(i-1, c), values[i] + f(i-1, c - weights[i]) if it fits )

Base case: when only the first item is left (i == 0), take it if it fits, else skip it: f(0, c) = values[0] if weights[0] <= c else 0. Unused capacity costs nothing — you are never forced to fill the bag.

The three versions

Start with plain recursion, straight from the recurrence — nothing else going on:

python
def knapsack(weights, values, W):
    n = len(weights)

    def f(i, c):
        if i == 0:                                           # only the first item is left
            return values[0] if weights[0] <= c else 0
        best = f(i - 1, c)                                   # skip
        if weights[i] <= c:                                  # take, if it fits
            best = max(best, values[i] + f(i - 1, c - weights[i]))
        return best

    return f(n - 1, W)

This is correct but slow. Each call splits into two — skip and take — so the tree has about 2ⁿ leaves. Yet only n × (W+1) different (i, c) pairs exist inside it, so the same pairs are recomputed again and again. Fix it with a notebook: before computing f(i, c), check whether it is already stored; if so, return it; if not, compute it once and save it. That drops the work to one computation per state, O(N × W):

python
def knapsack(weights, values, W):
    n = len(weights)
    memo = {}

    def f(i, c):
        if i == 0:
            return values[0] if weights[0] <= c else 0
        if (i, c) in memo: return memo[(i, c)]
        best = f(i - 1, c)
        if weights[i] <= c:
            best = max(best, values[i] + f(i - 1, c - weights[i]))
        memo[(i, c)] = best
        return best

    return f(n - 1, W)

Now drop the recursion and fill a table instead. Let dp[i][c] hold what f(i, c) returned: the best value from items 0 through i within capacity c. Read the recurrence as an assignment — swap each f(...) call for a dp[...] read:

dp[i][c] = max( dp[i-1][c], values[i] + dp[i-1][c - weights[i]] if it fits )

Each row i reads only row i-1, so fill the rows top to bottom (i = 0, 1, 2, ...). The base case copies straight over: the recursion returned values[0] if weights[0] <= c else 0 at i == 0, so that is row 0. The loop then starts at row i = 1, and the answer is dp[n-1][W]:

python
def knapsack(weights, values, W):
    n = len(weights)
    dp = [[0] * (W + 1) for _ in range(n)]

    for c in range(W + 1):                                   # row 0: only the first item
        dp[0][c] = values[0] if weights[0] <= c else 0

    for i in range(1, n):
        for c in range(W + 1):
            dp[i][c] = dp[i - 1][c]                          # skip
            if weights[i] <= c:
                dp[i][c] = max(dp[i][c],
                               values[i] + dp[i - 1][c - weights[i]])

    return dp[n - 1][W]
One row, swept backwards

Row i only ever reads row i-1, so keep one array. The capacity sweep must run downward, because dp[c - weight] must still hold the previous row's value — the world where this item has not been used yet.

python
def knapsack(weights, values, W):
    dp = [0] * (W + 1)
    for w, v in zip(weights, values):
        for c in range(W, w - 1, -1):        # downward keeps each item single-use
            dp[c] = max(dp[c], v + dp[c - w])
    return dp[W]
Crucial Noteflip that inner loop to ascending and you have silently solved a different problem — the unbounded knapsack, where each item may be taken any number of times, because dp[c - w] would already include this item. It will not crash and it will not look wrong; it will just return a larger number than the truth. Whenever you compress a knapsack-shaped table to one row, stop and ask which world your read is coming from.
Worked Example:W = 10, items (6, 30), (5, 24), (5, 24)
Using the one-row version, dp starts as all zeros.
- Item (w=6, v=30): sweeping c from 10 down to 6, each dp[c] becomes max(0, 30 + dp[c-6]) = 30. Row: capacities 0–5 hold 0, capacities 6–10 hold 30.
- Item (w=5, v=24): sweeping c from 10 down to 5. At c=10: max(30, 24 + dp[5]) = max(30, 24 + 0) = 30. At c=6: max(30, 24 + dp[1]) = 30. At c=5: max(0, 24 + dp[0]) = 24. Row now: 0–4 hold 0, 5 holds 24, 6–9 hold 30, 10 holds 30.
- Item (w=5, v=24): at c=10: max(30, 24 + dp[5]) = max(30, 24 + 24) = 48. There it is — dp[5] was 24 from the previous item, so the two lesser items combine.

Answer 48, beating the greedy 30 exactly as predicted.

Where this pattern shows up wearing disguises

The knapsack skeleton is one of the highest-yield shapes in interviews, and it is almost never introduced as "here is a bag". The signals to watch for:

- There is a budget — weight, money, time, a count of moves, a number of changes allowed — that decisions consume and cannot exceed.
- Each item is a yes/no decision rather than a quantity.
- You are maximising or minimising a total, or counting arrangements, subject to that budget.
- The budget's numeric bound is small (thousands, not billions). This is the giveaway, because the running time is proportional to it.

That last point deserves a warning. O(N × W) is called pseudo-polynomial: it is linear in the value of W, but W is written in only about log W digits, so doubling the number of digits squares the running time. If a problem gives a capacity of a billion, this table is not the intended solution no matter how knapsack-shaped it looks. The constraint bounds are telling you which technique is expected — read them before you choose.

This is the take-or-skip skeleton once more, now with max because we want the best value. Swap the combining operation and it answers different questions: or asks "is it possible", + counts the arrangements. The state and the walk stay the same; only that one operation changes.

Exponential Try Every Subset
O(N × W) Time · O(N × W) Space Memoisation
O(N × W) Time · O(N × W) Space Tabulation
O(N × W) Time · O(W) Space One Row