Solving the 0-1 Knapsack Problem in Python using Recursion

I have seen the knapsack problem show up in quite a few technical interviews. It is one of those problems that feels tricky at first but becomes much clearer once you see the recursive pattern underneath. In this tutorial, I will walk through the 0-1 knapsack problem, show you how to think about it recursively, and get a working Python implementation that you can actually use.

The core idea is this: you have a set of items, each with a weight and a value. You have a knapsack that can hold only so much weight. Your goal is to pick the combination of items that maximizes total value without exceeding the capacity. It shows up everywhere from resource allocation to combinatorial optimization. Let me show you how to solve it.

TLDR

  • The 0-1 knapsack problem asks: pick items with max total value without exceeding a weight limit
  • The recursive solution tries both including and excluding each item, then takes the better outcome
  • The base case fires when there are no items left or no capacity remaining
  • For weights=[1,2,3,4], prices=[50,200,150,100], capacity=7, the maximum value is 400
  • Recursion is easy to understand but slow for large inputs. Memoization fixes that

What Is the 0-1 Knapsack Problem?

The name sounds fancy but the setup is simple. Imagine a thief who broke into a jewelry store. The thief has a knapsack that can hold at most W kilograms. There are n items on display, each with a specific weight and a specific value. The thief wants to maximize the total value of items stolen while keeping the total weight under W.

Each item can only be taken once, hence the “0-1” in the name. You either take an item or you leave it. There is no partial take. This is what makes the problem interesting and what makes the brute-force approach blow up fast.

Here is the basic setup in Python:


weights = [1, 2, 3, 4]    # weight of each item
prices  = [50, 200, 150, 100]  # value of each item
n = 4               # number of items
capacity = 7        # maximum weight the knapsack can hold

Explain code: Three lists hold the item data, n is the total item count, and capacity is the weight limit. These are the inputs to the knapsack function.

Output:


# No output from the setup itself - these are inputs to the function below

The Recursive Approach

Here is the key insight that makes recursion work for this problem. For any item i, you have exactly two choices:

  • Include the item. You add its price to your total, subtract its weight from remaining capacity, and move to the next item.
  • Exclude the item. You simply move to the next item with the same capacity.

The answer for the current state is whichever choice gives you a higher total price. This is the recursive formulation.

Here is the recursive function:


def knapsack(n, capacity, weights, prices):
    # Base case: no items left or no capacity left
    if n == 0 or capacity == 0:
        return 0

    # If the current item weighs more than remaining capacity, skip it
    if weights[n - 1] > capacity:
        return knapsack(n - 1, capacity, weights, prices)

    # Otherwise, try both choices and take the better one
    with_item = prices[n - 1] + knapsack(
        n - 1,
        capacity - weights[n - 1],
        weights,
        prices
    )
    without_item = knapsack(n - 1, capacity, weights, prices)

    return max(with_item, without_item)

Explain code: The function checks base cases first. When the current item is too heavy for the remaining capacity, it skips the item. Otherwise it recursively compares the total value from including the item versus excluding it, returning whichever is higher.

Output:


result = knapsack(n, capacity, weights, prices)
print(result)
# 400

Explain code: Calling knapsack(4, 7, [1,2,3,4], [50,200,150,100]) returns 400. The optimal selection is items with weights 1, 2, and 3 (values 50, 200, 150) for a total value of 400 with weight 6.

Running the Full Example

Here is the complete runnable script with the example inputs:


def knapsack(n, capacity, weights, prices):
    if n == 0 or capacity == 0:
        return 0

    if weights[n - 1] > capacity:
        return knapsack(n - 1, capacity, weights, prices)

    with_item = prices[n - 1] + knapsack(
        n - 1,
        capacity - weights[n - 1],
        weights,
        prices
    )
    without_item = knapsack(n - 1, capacity, weights, prices)

    return max(with_item, without_item)


weights = [1, 2, 3, 4]
prices  = [50, 200, 150, 100]
n = 4
capacity = 7

result = knapsack(n, capacity, weights, prices)
print(f"Maximum value: {result}")

Explain code: This is the complete solution combining the recursive function with the example inputs. The print call shows the result.

Output:


Maximum value: 400

Visualizing the Decision Tree

It helps to see the recursion as a decision tree. At each step, you branch into two: include or exclude. Here is a smaller example with 3 items and capacity 3 so you can trace through it by hand:


# Small example to visualize the decision process
weights_small = [2, 3, 1]
prices_small  = [10, 15, 8]
capacity_small = 3

result_small = knapsack(3, capacity_small, weights_small, prices_small)
print(f"Maximum value for small case: {result_small}")

Explain code: A smaller knapsack with 3 items and capacity 3. Item 0 weighs 2 and is worth 10, item 1 weighs 3 and is worth 15, item 2 weighs 1 and is worth 8. The optimal pick is item 0 plus item 2 for value 18.

Output:


Maximum value for small case: 18

Why the Recursive Solution Is Slow

The pure recursive solution recalculates the same subproblems over and over. For n items, the number of calls grows as 2 to the power of n in the worst case. This is exponential time complexity. For 30 items, you are already looking at over a billion recursive calls.

The fix is memoization: store the results of already-computed subproblems so you never recompute them. Python makes this straightforward with a cache decorator:


from functools import lru_cache

@lru_cache(maxsize=None)
def knapsack_memo(n, capacity, weights, prices):
    if n == 0 or capacity == 0:
        return 0

    if weights[n - 1] > capacity:
        return knapsack_memo(n - 1, capacity, weights, prices)

    with_item = prices[n - 1] + knapsack_memo(
        n - 1,
        capacity - weights[n - 1],
        weights,
        prices
    )
    without_item = knapsack_memo(n - 1, capacity, weights, prices)

    return max(with_item, without_item)


weights = [1, 2, 3, 4]
prices  = [50, 200, 150, 100]
result_memo = knapsack_memo(4, 7, tuple(weights), tuple(prices))
print(f"Maximum value (memoized): {result_memo}")

Explain code: The lru_cache decorator stores every result for each unique set of arguments. Tuples are used instead of lists as the cache keys since lists are not hashable. The result is the same 400 but computed much faster.

Output:


Maximum value (memoized): 400

Tracing Through knapsack(4, 7, [1,2,3,4], [50,200,150,100])

Here is a concise trace of how the recursion resolves for the main example. The items are indexed 0 through 3:

  • knapsack(4, 7) considers all 4 items with capacity 7. Item 3 weighs 4, price 100. Both include and exclude paths are viable.
  • Include item 3: 100 + knapsack(3, 3). Item 2 weighs 3, price 150, fits exactly in capacity 3. So 150 + knapsack(2, 0) = 150 + 0 = 150.
  • Exclude item 3: knapsack(3, 7). Item 2 weighs 3, price 150. Include it: 150 + knapsack(2, 4). Item 1 weighs 2, price 200, fits in capacity 4. Include: 200 + knapsack(1, 2). Item 0 weighs 1, price 50, fits in capacity 2. Include: 50 + knapsack(0, 1) = 50 + 0 = 50. Exclude item 0: knapsack(0, 2) = 0. Max for (1, 2) is 50. So 200 + 50 = 250 for including item 1. Exclude item 1: knapsack(1, 4). Item 0 weighs 1, price 50. Include: 50 + knapsack(0, 3) = 50. Exclude: knapsack(0, 4) = 0. Max is 50. The larger of 250 and 50 is 250. So knapsack(2, 4) = 250. Including item 2 gives 150 + 250 = 400.
  • Back at the root: include item 3 gave 150. Exclude item 3 gave 400. The max of 150 and 400 is 400.

The optimal selection is items 0, 1, and 2 (weights 1+2+3=6, values 50+200+150=400). The recursion correctly identifies this combination.

FAQ

Q: What is the difference between 0-1 knapsack and fractional knapsack?

The 0-1 knapsack problem requires taking the whole item or leaving it. The fractional knapsack problem allows taking a fraction of an item. Fractional knapsack is solvable with a greedy approach using value-to-weight ratios. 0-1 knapsack requires exponential time with brute force or polynomial time with dynamic programming.

Q: What is the time complexity of the recursive solution?

The naive recursive solution has exponential time complexity O(2 to the power of n) because each item spawns two recursive branches. With memoization, the complexity becomes O(n times W) where n is the number of items and W is the knapsack capacity.

Q: Can this be solved iteratively with dynamic programming?

Yes. A 2D table dp[i][w] stores the maximum value achievable using the first i items with capacity w. The iterative DP version fills this table bottom-up and avoids the call stack overhead of recursion.

Q: What happens when all items weigh more than the capacity?

The base case catches this. When the capacity becomes 0 or there are no items left to consider, the function returns 0. No items can be selected, so the maximum value is 0.

Q: Why use tuple(weights) and tuple(prices) with lru_cache?

The lru_cache decorator uses function arguments as dictionary keys. Lists are mutable and not hashable, so they cannot be used directly. Converting to tuples provides immutable, hashable equivalents that work as cache keys.

I hope the knapsack problem makes more sense now. The recursive approach is the foundation for the dynamic programming solution and it maps cleanly to how the problem is naturally defined. If you are preparing for interviews, I recommend implementing both the memoized version and the bottom-up DP version to solidify your understanding.

Ninad
Ninad

A Python and PHP developer turned writer out of passion. Over the last 6+ years, he has written for brands including DigitalOcean, DreamHost, Hostinger, and many others. When not working, you'll find him tinkering with open-source projects, vibe coding, or on a mountain trail, completely disconnected from tech.

Articles: 132