The Algorithms - Python Python TheAlgorithms/Python

How the 0/1 Knapsack DP Works

Fill a capacity-by-item table bottom-up, then trace back through it to recover which items go in the bag.

6 stops ~20 min Verified 2026-05-05
What you will learn
  • Why the 0/1 knapsack problem is NP-complete in general but solvable in pseudo-polynomial time with DP
  • How the module docstring states the scope restriction: integer weights only
  • How mf_knapsack uses top-down memoization with a global table initialized to -1
  • How the bottom-up knapsack function fills a 2-D dp table row by row, column by column
  • What the recurrence relation max(include item i, exclude item i) encodes at each cell
  • How _construct_solution traces the filled table backward to recover one optimal item subset
  • What the __main__ block asserts about a concrete 4-item, capacity-6 example
Prerequisites
  • Comfort reading nested Python loops
  • Familiarity with the concept of recursion and memoization
  • No prior knowledge of dynamic programming or knapsack required
The story behind the 0/1 knapsack problem

The knapsack problem takes its name from a thought experiment: a thief with a bag of fixed capacity wants to fill it with the most valuable combination of items, where each item has a weight and a value, and each item can either go in the bag or stay out. No fractions. The 0/1 constraint is what makes it hard. Tobias Dantzig (father of George Dantzig, who formulated the simplex method) included an early version of the problem in his 1930 book on number theory. Richard Bellman identified the problem as a canonical case for dynamic programming in his 1957 monograph, and the pseudo-polynomial DP solution he described is the one this file implements.

The problem is weakly NP-hard for arbitrary integer weights (the pseudo-polynomial O(nW) DP solution is precisely why it is not strongly NP-complete). But when weights are non-negative integers bounded by some capacity W, a bottom-up table of size (n+1) x (W+1) fills in O(nW) time, which is polynomial in the input size if W stays bounded. That is the pseudo-polynomial insight. Cousin problems include subset-sum (can values sum to a target?), bin-packing (fit items into the fewest bins), and the partition problem (split a set into two equal-sum subsets). All reduce to or from 0/1 knapsack. The fractional variant, where items can be cut, is solvable greedily in O(n log n) and lives in greedy_methods/fractional_knapsack.py.

1 / 6

Module Docstring: The Problem Statement and the Constraint

dynamic_programming/knapsack.py:1

The module docstring states the problem and immediately restricts scope: only integer weights, only the 0/1 variant, solved by DP.

The docstring is short for a reason. Lines 5 and 6 do the work the reader needs before touching any code: they rule out the fractional case and the unbounded case. Real-valued weights break the DP table because you cannot index a continuous capacity axis with integers. Unbounded items (where each item may be taken multiple times) need a different recurrence. The file solves one specific variant, and the docstring says so. That narrowing makes the implementation below unambiguous: every function in this file assumes integer capacities and each item is either included once or not at all. The fractional variant is handled separately at greedy_methods/fractional_knapsack.py. Knowing the boundary before reading the code tells you exactly what the table fills and what the traceback recovers.

Key takeaway

The file solves the integer-weights 0/1 variant only. Real-valued weights and unbounded items need different approaches.

"""
Given weights and values of n items, put these items in a knapsack of
capacity W to get the maximum total value in the knapsack.

Note that only the integer weights 0-1 knapsack problem is solvable
using dynamic programming.
"""
2 / 6

mf_knapsack: Top-Down Memoization with a Global Table

dynamic_programming/knapsack.py:10

mf_knapsack is a top-down memoized recursion that stores results in a global 2-D array initialized to -1, computing only the subproblems it actually needs.

Top-down memoization and bottom-up tabulation solve the same recurrence; the difference is which subproblems get computed. mf_knapsack uses the global array f, initialized in the __main__ block with -1 for every cell that has not yet been computed. Line 17 is the memoization check: if f[i][j] is still -1, the subproblem is unseen and the function computes it. If f[i][j] already holds a non-negative value, the function returns it immediately. Line 18 covers the case where item i cannot fit at all (its weight exceeds remaining capacity j), so the best value is the same as with the previous items. Lines 21 through 23 encode the core decision: take the maximum of excluding item i (first branch) and including it (second branch, which reduces the remaining capacity by wt[i-1] and adds val[i-1]). The result is written back to f[i][j] before return so future calls skip the recursion.

Key takeaway

The -1 sentinel distinguishes unseen subproblems from computed ones. Line 17 is the memoization gate; lines 21-23 are the 0/1 inclusion decision.

def mf_knapsack(i, wt, val, j):
    """
    This code involves the concept of memory functions. Here we solve the subproblems
    which are needed unlike the below example
    F is a 2D array with ``-1`` s filled up
    """
    global f  # a global dp table for knapsack
    if f[i][j] < 0:
        if j < wt[i - 1]:
            val = mf_knapsack(i - 1, wt, val, j)
        else:
            val = max(
                mf_knapsack(i - 1, wt, val, j),
                mf_knapsack(i - 1, wt, val, j - wt[i - 1]) + val[i - 1],
            )
        f[i][j] = val
    return f[i][j]
3 / 6

knapsack: Bottom-Up Table Fill

dynamic_programming/knapsack.py:29

Bottom-up knapsack fills an (n+1) by (W+1) table row by row; each cell is the max value with the first i items at capacity w.

Bottom-up DP fills every subproblem regardless of whether it is needed, and in exchange it avoids the overhead of recursion and global state. Line 30 allocates the table: an (n+1)-row, (W+1)-column list of zeroes. Row 0 is the base case (zero items, any capacity, zero value). Row i and column w hold the best achievable value using items 1 through i at capacity w. The outer loop walks items; the inner loop walks capacities. Line 34 checks whether item i-1 fits at the current capacity. If it fits, line 35 applies the inclusion/exclusion max: either take the item (using the previously computed value at reduced capacity) or skip it (keeping the row above). If it cannot fit, line 37 copies the row above unchanged. Line 39 returns the optimal value and the full table so the caller can trace back which items produced it.

Key takeaway

Row 0 is all zeroes. Each subsequent row adds one item decision. Line 35 is the inclusion/exclusion max that drives the fill.

def knapsack(w, wt, val, n):
    dp = [[0] * (w + 1) for _ in range(n + 1)]

    for i in range(1, n + 1):
        for w_ in range(1, w + 1):
            if wt[i - 1] <= w_:
                dp[i][w_] = max(val[i - 1] + dp[i - 1][w_ - wt[i - 1]], dp[i - 1][w_])
            else:
                dp[i][w_] = dp[i - 1][w_]

    return dp[n][w_], dp
4 / 6

knapsack_with_example_solution: Doctest and Input Validation

dynamic_programming/knapsack.py:42

The public wrapper validates input types, calls knapsack to fill the table, then calls _construct_solution to recover one optimal item subset.

This is the file's public API and the only function with a doctest, which is why it is the right stop for understanding the contract. The three examples cover the meaningful test surface. knapsack_with_example_solution(10, [1, 3, 5, 2], [10, 20, 100, 22]) returns (142, {2, 3, 4}): items at indices 2, 3, and 4 (weights 3, 5, 2) fit within capacity 10 and yield value 20+100+22=142. The second example at capacity 6 returns (8, {3, 4}). The third example deliberately passes mismatched list lengths and asserts a ValueError, which the contribution guide requires for all functions that can receive bad input. The doctest for the error case uses the Traceback (most recent call last): ... pattern that pytest's doctest runner understands as an expected exception.

Key takeaway

The doctest pins two correct outputs and one expected ValueError. The error case uses the standard doctest exception pattern that CI validates on every push.

def knapsack_with_example_solution(w: int, wt: list, val: list):
    """
    Solves the integer weights knapsack problem returns one of
    the several possible optimal subsets.

    Parameters
    ----------

    * `w`: int, the total maximum weight for the given knapsack problem.
    * `wt`: list, the vector of weights for all items where ``wt[i]`` is the weight
       of the ``i``-th item.
    * `val`: list, the vector of values for all items where ``val[i]`` is the value
      of the ``i``-th item

    Returns
    -------

    * `optimal_val`: float, the optimal value for the given knapsack problem
    * `example_optional_set`: set, the indices of one of the optimal subsets
      which gave rise to the optimal value.

    Examples
    --------

    >>> knapsack_with_example_solution(10, [1, 3, 5, 2], [10, 20, 100, 22])
    (142, {2, 3, 4})
    >>> knapsack_with_example_solution(6, [4, 3, 2, 3], [3, 2, 4, 4])
    (8, {3, 4})
    >>> knapsack_with_example_solution(6, [4, 3, 2, 3], [3, 2, 4])
    Traceback (most recent call last):
        ...
    ValueError: The number of weights must be the same as the number of values.
    But got 4 weights and 3 values
    """
5 / 6

_construct_solution: Traceback Through the Filled Table

dynamic_programming/knapsack.py:103

_construct_solution walks the dp table backward. When a cell differs from the row above, item i was included; otherwise it was skipped.

Filling the table gives the optimal value; recovering which items produced it requires a separate backward pass. The function starts at cell (n, W) and asks a question at each step: did item i contribute to the optimal solution at this capacity? Line 127 compares the value in the row above (dp[i-1][j]) to the current cell (dp[i][j]). If they are equal, item i was not taken and the function recurses with i-1, keeping the same capacity j. If they differ, item i was included: line 130 adds i to the output set and recurses with capacity reduced by wt[i-1], because including item i consumed that weight. The base case on line 126 stops when either index reaches zero. Because multiple optimal subsets may exist (ties in the table), this traceback recovers one of them, not all of them, which is what the docstring says.

Key takeaway

Traceback reads the table backward: equal rows mean item was skipped; differing rows mean item was included and capacity shrinks. One valid subset, not all of them.

def _construct_solution(dp: list, wt: list, i: int, j: int, optimal_set: set):
    """
    Recursively reconstructs one of the optimal subsets given
    a filled DP table and the vector of weights

    Parameters
    ----------

    * `dp`: list of list, the table of a solved integer weight dynamic programming
      problem
    * `wt`: list or tuple, the vector of weights of the items
    * `i`: int, the index of the item under consideration
    * `j`: int, the current possible maximum weight
    * `optimal_set`: set, the optimal subset so far. This gets modified by the function.

    Returns
    -------

    ``None``
    """
    # for the current item i at a maximum weight j to be part of an optimal subset,
    # the optimal value at (i, j) must be greater than the optimal value at (i-1, j).
    # where i - 1 means considering only the previous items at the given maximum weight
    if i > 0 and j > 0:
        if dp[i - 1][j] == dp[i][j]:
            _construct_solution(dp, wt, i - 1, j, optimal_set)
        else:
            optimal_set.add(i)
            _construct_solution(dp, wt, i - 1, j - wt[i - 1], optimal_set)
6 / 6

The __main__ Block: A Concrete Verification

dynamic_programming/knapsack.py:134

The __main__ block runs both solvers on a 4-item, capacity-6 instance and asserts the answers match before printing them.

The block demonstrates the three functions together on a single concrete instance and uses assert to make the expected results machine-checkable. Items have weights [4, 3, 2, 3] and values [3, 2, 4, 4], and the bag holds capacity 6. Line 142 initializes the global table f that mf_knapsack reads: row 0 is all zeroes (the boundary condition where no items have been considered), and rows 1 through n+1 start at 0 in column 0 and -1 everywhere else. Both knapsack and mf_knapsack run on the same instance and print the same optimal value, confirming the two approaches agree. Lines 150 and 151 then assert that the optimal value is 8 and the recovered subset is items 3 and 4, which weigh 2+3=5 and yield value 4+4=8. Any regression in either the table fill or the traceback fails the assertion before the print lines execute.

Key takeaway

The block initializes the global memoization table, runs both solvers, and asserts the optimal value and item subset before printing. Both approaches agree on the same instance.

if __name__ == "__main__":
    """
    Adding test case for knapsack
    """
    val = [3, 2, 4, 4]
    wt = [4, 3, 2, 3]
    n = 4
    w = 6
    f = [[0] * (w + 1)] + [[0] + [-1] * (w + 1) for _ in range(n + 1)]
    optimal_solution, _ = knapsack(w, wt, val, n)
    print(optimal_solution)
    print(mf_knapsack(n, wt, val, w))  # switched the n and w

    # testing the dynamic programming problem with example
    # the optimal subset for the above example are items 3 and 4
    optimal_solution, optimal_subset = knapsack_with_example_solution(w, wt, val)
    assert optimal_solution == 8
    assert optimal_subset == {3, 4}
    print("optimal_value = ", optimal_solution)
    print("An optimal subset corresponding to the optimal value", optimal_subset)
Your codebase next

Create code tours for your project

Intraview lets AI create interactive walkthroughs of any codebase. Install the free VS Code extension and generate your first tour in minutes.

Install Intraview Free