13.7 Knapsack

The knapsack problem occupies a special place in dynamic programming.

13.7 Knapsack

The knapsack problem occupies a special place in dynamic programming. It is one of the earliest problems many programmers encounter, yet it contains nearly every important dynamic programming concept: state design, recurrence construction, resource constraints, optimization objectives, space reduction, and problem variations.

More importantly, knapsack serves as a modeling framework. A surprising number of practical optimization problems can be transformed into knapsack variants. Budget allocation, project selection, cargo loading, portfolio construction, scheduling, resource planning, and capacity management often reduce to choosing a subset of options subject to constraints.

This section develops the classical 0/1 knapsack problem and introduces the techniques that form the foundation for more advanced variants.

Problem

Given:

  • (n) items
  • weight (w_i)
  • value (v_i)
  • knapsack capacity (W)

Select items whose total weight does not exceed (W) while maximizing total value.

Each item may be chosen at most once.

Example:

Item Weight Value
A 2 3
B 3 4
C 4 5
D 5 8

Capacity:

$$ W = 8 $$

Goal:

maximize value
subject to weight limit

Why Greedy Fails

A natural idea is to choose the most valuable item first.

Suppose:

Item Weight Value
A 10 100
B 9 99
C 1 1

Capacity:

$$ 10 $$

Choosing A yields:

$$ 100 $$

Choosing B and C yields:

$$ 100 $$

Now modify the values slightly:

Item Weight Value
A 10 100
B 9 99
C 1 2

Greedy chooses A:

$$ 100 $$

Optimal solution:

$$ 99 + 2 = 101 $$

Local choices do not guarantee global optimality.

This immediately suggests dynamic programming.

State Design

The critical observation is that every decision depends on two facts:

  • which items remain available
  • how much capacity remains

A natural state is:

$$ dp[i][c] $$

Meaning:

maximum value obtainable
using first i items
with capacity c

This state captures all information required for future decisions.

Understanding the Subproblem

Consider:

$$ dp[5][20] $$

This asks:

Using only the first five items,
what is the largest value obtainable
without exceeding capacity 20?

Notice how this mirrors the original problem.

Dynamic programming succeeds because large problems can be decomposed into smaller versions of the same question.

Deriving the Recurrence

Focus on item (i).

Two possibilities exist.

Case 1: Skip the Item

The solution remains:

$$ dp[i-1][c] $$

Case 2: Take the Item

If:

$$ weight_i \le c $$

then:

$$ value_i + dp[i-1][c-weight_i] $$

becomes possible.

The recurrence is:

$$ dp[i][c] = \max \left( dp[i-1][c], ; dp[i-1][c-weight_i]+value_i \right) $$

This simple recurrence is the heart of the knapsack algorithm.

Table Construction

Consider:

Item Weight Value
A 2 3
B 3 4
C 4 5

Capacity:

$$ 5 $$

Create a table:

Items 0 1 2 3 4 5
0 items 0 0 0 0 0 0
A 0 0 3 3 3 3
A,B 0 0 3 4 4 7
A,B,C 0 0 3 4 5 7

The answer appears in:

$$ dp[n][W] $$

which equals:

$$ 7 $$

The optimal solution selects A and B.

Implementation

func Knapsack(
    weights []int,
    values []int,
    capacity int,
) int {

    n := len(weights)

    dp := make([][]int, n+1)

    for i := range dp {
        dp[i] = make([]int, capacity+1)
    }

    for i := 1; i <= n; i++ {

        weight := weights[i-1]
        value := values[i-1]

        for c := 0; c <= capacity; c++ {

            dp[i][c] = dp[i-1][c]

            if weight <= c {

                candidate :=
                    dp[i-1][c-weight] + value

                if candidate > dp[i][c] {
                    dp[i][c] = candidate
                }
            }
        }
    }

    return dp[n][capacity]
}

This implementation follows the recurrence directly.

Complexity Analysis

State count:

$$ (n+1)(W+1) $$

Each state performs constant work.

Time complexity:

$$ O(nW) $$

Memory complexity:

$$ O(nW) $$

Notice that complexity depends on capacity rather than the number of bits needed to represent capacity.

This distinction becomes important when analyzing large instances.

Reconstructing the Selected Items

Often the value alone is insufficient.

We may also want to know which items were chosen.

Starting from:

$$ dp[n][W] $$

compare:

$$ dp[i][c] $$

with:

$$ dp[i-1][c] $$

If they differ:

item i was selected

Move to:

$$ (i-1,; c-weight_i) $$

Otherwise:

item i was skipped

Move to:

$$ (i-1,; c) $$

This process reconstructs an optimal solution.

Space Optimization

Observe the recurrence carefully:

$$ dp[i][c] $$

depends only on:

$$ dp[i-1][*] $$

The entire previous table is unnecessary.

Store only one row:

dp := make([]int, capacity+1)

Process capacities in reverse order:

for _, item := range items {

    for c := capacity; c >= item.Weight; c-- {

        dp[c] = max(
            dp[c],
            dp[c-item.Weight]+item.Value,
        )
    }
}

Memory decreases from:

$$ O(nW) $$

to:

$$ O(W) $$

while preserving correctness.

Why Reverse Iteration Matters

Suppose capacity increases from left to right.

Then:

current row values

can accidentally influence later computations.

The same item may be selected multiple times.

Reverse iteration guarantees:

every item contributes at most once

This property is essential for 0/1 knapsack.

Common Mistakes

Forward Capacity Iteration

for c := weight; c <= capacity; c++

This produces a different algorithm.

Items can be reused multiple times.

The result becomes unbounded knapsack rather than 0/1 knapsack.

Incorrect State Meaning

Some implementations mix:

remaining capacity

and:

used capacity

Choose one interpretation and maintain it consistently.

Missing Base Cases

The first row:

$$ dp[0][c] $$

must represent:

no items available

Therefore every entry equals zero.

Oversized Tables

When capacity is large, an (O(nW)) table may exceed available memory.

The one-dimensional optimization often becomes necessary.

Recognizing Knapsack Problems

Many problems hide the knapsack structure behind different terminology.

Typical clues include:

  • fixed resource budget
  • capacity constraint
  • weight or cost limit
  • subset selection
  • maximize benefit
  • minimize cost
  • choose each option at most once

Examples:

  • project portfolio selection
  • advertisement budgeting
  • cloud resource allocation
  • shipping optimization
  • capital investment planning
  • feature prioritization

Whenever you see:

choose a subset
subject to a resource constraint

knapsack should immediately come to mind.

Variants

Several important algorithms grow directly from 0/1 knapsack:

  • Unbounded Knapsack
  • Bounded Knapsack
  • Multiple Knapsack
  • Multi-Dimensional Knapsack
  • Subset Sum
  • Partition Equal Subset Sum
  • Target Sum
  • Coin Change

Many advanced dynamic programming problems are simply knapsack variations with different state interpretations.

Takeaway

The 0/1 knapsack problem demonstrates the core dynamic programming workflow: identify the information needed for future decisions, define a state around that information, enumerate all valid choices, and construct a recurrence that selects the best outcome. The resulting algorithm runs in (O(nW)) time, supports efficient solution reconstruction, and can be optimized to (O(W)) memory. More importantly, knapsack provides a reusable modeling framework for a large class of constrained optimization problems that appear throughout algorithm design.