Skip to main content

The Knapsack Problem: A Comprehensive Guide to Dynamic Programming

Published: April 24, 2026 Updated: May 8, 2026 Larry Qu 12 min read

Introduction

The Knapsack Problem is one of the most famous challenges in combinatorial optimization and computer science fundamentally. At its core, the premise is deceptively simple: You have a knapsack (backpack) with a strict maximum weight capacity. You are presented with a set of items, each possessing a specific weight and a specific monetary value.

Your goal? Determine the exact combination of items to pack into the knapsack that maximizes the final total value without exceeding the weight limit.

While it sounds like a simple puzzle, the Knapsack Problem forms the theoretical foundation for countless vital systems in software engineering, such as:

  • Cloud Computing Resource Allocation: Maximizing the “value” (priority) of jobs packed into a single server CPU “weight” (capacity).
  • Financial Portfolio Optimization: Selecting the best investments given a strict capital limit.
  • Logistics and Freight Routing: Loading delivery trucks or shipping containers with the highest-value cargo combinations.

The Two Primary Variants

Before writing code, we must distinguish between the two major variants of this algorithm, because they require radically different approaches.

1. The Fractional Knapsack Problem (The Easy Variant)

In the fractional variant, you are allowed to break items into pieces. Imagine you are stuffing your bag with gold dust, silver dust, and flour. If you hit your weight limit, you can simply scoop exactly 0.5 lbs of gold to perfectly fill the bag.

Solution: This is solved trivially using a Greedy Algorithm. You simply calculate the value-to-weight ratio of each item, sort them descending, and shove as much of the highest-ratio item into your bag as possible until full.

2. The 0/1 Knapsack Problem (The Hard Variant)

In the 0/1 Knapsack Problem, items are indivisible. You cannot break them. You either take the item totally (1) or you leave it behind totally (0). Imagine you are packing a Guitar, a Keyboard, and a Laptop. You cannot cut the guitar in half.

Solution: The Greedy Algorithm universally fails here. Sorting by value-to-weight ratio might cause you to pick a highly efficient small item, but doing so might permanently block you from taking a slightly-less-efficient but massively valuable large item. We must use Dynamic Programming (DP) to explore overlapping subproblems.


Dynamic Programming Approach (The 2D Matrix)

Dynamic Programming solves 0/1 Knapsack by breaking it down. For every single item, we ask a binary question: “If my bag could only hold W pounds right now, what is my maximum profit if I decide to INCLUDE this item, versus what is my profit if I EXCLUDE it?”

We build a 2D matrix (table) where:

  • Rows (i): Represent the items we are considering (from 1 to N).
  • Columns (w): Represent every possible weight capacity of the knapsack ranging from 0 up to Max_Weight.
  • Cells (dpi[w]): Store the maximum value achievable at that specific capacity w considering items up to i.

The Core DP State Transition Equation

The mathematical heart of the entire algorithm is this single line:

$$ DP[i][w] = \max( DP[i-1][w], \text{Value}_i + DP[i-1][w - \text{Weight}_i] ) $$

Translation into English: The best profit considering item i at capacity w is the maximum of two choices:

  1. Exclude Item $i$: We just inherit the best profit we had previously without this item: DP[i-1][w].
  2. Include Item $i$: We take the value of the new item (Value_i), and add it to the best profit of whatever capacity was leftover (w - Weight_i) before this item was considered.

Implementation in Python 3

Let’s implement the classic 2D array bottom-up tabular approach.

def knapsack_01_2d(max_weight: int, weights: list[int], values: list[int]) -> int:
    """
    Solves the 0/1 Knapsack Problem using a 2D DP table.
    """
    n = len(values)
    
    # Initialize a 2D DP matrix with dimensions (n+1) x (max_weight+1)
    # Pre-filled with mostly zeros
    dp = [[0 for _ in range(max_weight + 1)] for _ in range(n + 1)]

    # Build table in bottom-up manner
    for i in range(1, n + 1):
        for w in range(1, max_weight + 1):
            
            item_weight = weights[i-1]
            item_value = values[i-1]
            
            if item_weight <= w:
                # We have enough capacity to potentially take the item
                # Max of (NOT taking it) vs (TAKING it + value of leftover space)
                dp[i][w] = max(
                    dp[i-1][w],
                    item_value + dp[i-1][w - item_weight]
                )
            else:
                # The item is too heavy for the current running capacity 'w'.
                # We HAVE to exclude it and inherit the previous state.
                dp[i][w] = dp[i-1][w]

    # The final cell in the bottom-right corner holds the maximum profit
    return dp[n][max_weight]


# Usage Example
items = ["Guitar", "Keyboard", "Accordion"]
values = [1500, 3000, 2000]
weights = [1, 4, 3]
max_capacity = 4

max_profit = knapsack_01_2d(max_capacity, weights, values)
print(f"Maximum Profit: ${max_profit}")
# => Maximum Profit: $3500 (Takes Guitar + Accordion)
```python
---

## Complexity Analysis

### Time Complexity: $O(N \times W)$

Where `N` is the total number of distinct items, and `W` is the maximum weight capacity of the knapsack. Because we must iterate through and populate an entire matrix of this size, the algorithm executes in pseudo-polynomial time.

### Space Complexity: $O(N \times W)$ (Unoptimized)

Because we construct a 2D array, we consume memory proportional to the number of items multiplied by the weight. For massive knapsacks (e.g., a capacity of 100,000 lbs), this 2D array could hit memory limits locally.

---

## Space Optimization (1D Array Trick)

If you revisit the State Transition Equation carefully:
`DP[i][w] = max( DP[i-1][w], Value_i + DP[i-1][w - Weight_i] )`

You will notice that calculating row `i` **strictly only requires data from row `i-1`**. We don't need row `i-2`, `i-3`, etc. We can discard historical rows.

Even better: if we iterate through the capacities **backwards** (from `max_capacity` down to `0`), we can squash the entire 2D matrix into a single flat `1D` array of size `W`. Iterating backwards guarantees we are reading the `i-1` state from the left side of the array before we overwrite it with the fresh `i` calculation.

### Optimized Implementation in Go

This is the production-ready 1D optimized variant, cutting Space Complexity violently down from $O(N \times W)$ to merely **$O(W)$**.

```go
package main

import (
"fmt"
)

// max returns the larger of two ints
func max(a, b int) int {
if a > b {
return a
}
return b
}

// Knapsack1D solves 0/1 Knapsack with O(W) space
func Knapsack1D(capacity int, weights []int, values []int) int {
n := len(values)

// Create a single 1D slice spanning 0 to capacity
dp := make([]int, capacity+1)

// Iterate over every item
for i := 0; i < n; i++ {
// Iterate BACKWARDS through capacities
// Crucial: we stop as soon as w < weights[i] because we can't fit it anyway
for w := capacity; w >= weights[i]; w-- {
dp[w] = max(
dp[w],                           // Case 1: Exclude item
values[i] + dp[w-weights[i]],    // Case 2: Include item
)
}
}

return dp[capacity]
}

func main() {
values := []int{1500, 3000, 2000}
weights := []int{1, 4, 3}
capacity := 4

profit := Knapsack1D(capacity, weights, values)
fmt.Printf("Optimized 1D Maximum Profit: $%d\n", profit)
// => Optimized 1D Maximum Profit: $3500
}

Legacy Implementation (Ruby)

For historical reference within this repository, here is the basic unoptimized DP implementation mapped in Ruby:

def knapsack_ruby(max_weight, goods)
  # goods = [[value, weight], [value, weight]]
  
  # Initialize 2D matrix
  dp = Array.new(goods.size + 1) { Array.new(max_weight + 1, 0) }
  
  (1..goods.size).each do |i|
    item_val = goods[i-1][0]
    item_wt = goods[i-1][1]
    
    (1..max_weight).each do |w|
      if item_wt <= w
        dp[i][w] = [ 
          dp[i-1][w], 
          item_val + dp[i-1][w - item_wt] 
        ].max
      else
        dp[i][w] = dp[i-1][w]
      end
    end
  end
  
  dp[goods.size][max_weight]
end

goods = [ [1500, 1], [3000, 4], [2000, 3] ]
puts knapsack_ruby(4, goods)

Summary

The Knapsack Problem remains the definitive gateway into mastering Dynamic Programming, transforming an exponentially complex trial-and-error nightmare into an elegant structured tabular state machine.

Key Takeaways

  1. Greedy Traps: The Greedy method (sorting by value-to-weight ratio) intuitively feels right but categorically fails on the 0/1 variant because items are indivisible, leading to wasted potential space. It only works if you can break fractions of items.

  2. Dynamic Programming Necessity: The 0/1 variant rigorously requires tabulating state, eliminating redundant calculations of sub-problems (like calculating the optimal subset of 3 items over and over).

  3. The Core Recurrence: Whenever faced with purely binary (Take or Leave) resource allocation, remember the universal formula:

    DP[i][w] = max(
        Exclude,                              # Option 1
        Value_i + Include(w - Weight_i)       # Option 2
    )
    
  4. Space Complexity Mastery: By stepping down from the classical 2D n * w matrix and traversing the inner capacity loop backwards, you can optimize your memory profile violently down to a flat 1D array of size w, keeping your Cloud Lambda payloads impossibly lean.

Unbounded Knapsack

In the unbounded variant, each item can be selected any number of times. This changes the recurrence: instead of DP[i-1][w - weight_i] (which assumes each item used once), we use DP[i][w - weight_i] (allowing reuse):

def unbounded_knapsack(max_weight: int, weights: list[int], values: list[int]) -> int:
    """Unbounded knapsack: each item can be used multiple times."""
    dp = [0] * (max_weight + 1)

    for w in range(1, max_weight + 1):
        for i in range(len(weights)):
            if weights[i] <= w:
                dp[w] = max(dp[w], dp[w - weights[i]] + values[i])

    return dp[max_weight]


# Example: coin change (minimize coins for target amount)
def coin_change(coins: list[int], amount: int) -> int:
    """Minimum number of coins to make amount (unbounded)."""
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0

    for coin in coins:
        for a in range(coin, amount + 1):
            dp[a] = min(dp[a], dp[a - coin] + 1)

    return dp[amount] if dp[amount] != float('inf') else -1

Time Complexity: O(N * W) – same as 0/1 knapsack Space: O(W) – 1D array suffices

Fractional Knapsack (Greedy Algorithm)

Unlike the 0/1 variant, fractional knapsack allows taking portions of items, making it solvable with a greedy strategy:

def fractional_knapsack(max_weight: float, weights: list[float], values: list[float]) -> float:
    """Fractional knapsack: greedy by value/weight ratio."""
    items = [(values[i], weights[i], values[i] / weights[i]) for i in range(len(weights))]
    items.sort(key=lambda x: x[2], reverse=True)  # Sort by ratio descending

    total_value = 0.0
    remaining = max_weight

    for value, weight, ratio in items:
        if weight <= remaining:
            total_value += value
            remaining -= weight
        else:
            total_value += ratio * remaining
            break

    return total_value


# Example: resource allocation
items_value = [60, 100, 120]
items_weight = [10, 20, 30]
capacity = 50
print(f"Maximum fractional value: {fractional_knapsack(capacity, items_weight, items_value)}")
# => Maximum fractional value: 240.0

Time Complexity: O(N log N) for sorting Space: O(N) for items array Key Insight: Sorting by value-to-weight ratio is optimal when items are divisible.

Bounded Knapsack

The bounded variant limits each item to a specific count (neither 1 nor infinite). It can be solved by converting each item into multiple 0/1 items or using binary splitting:

def bounded_knapsack(max_weight: int, weights: list[int], values: list[int], counts: list[int]) -> int:
    """Bounded knapsack: each item available in limited quantity."""
    n = len(weights)
    dp = [0] * (max_weight + 1)

    for i in range(n):
        # Binary splitting: decompose count into powers of 2
        remaining = counts[i]
        k = 1
        while remaining > 0:
            take = min(k, remaining)
            w = weights[i] * take
            v = values[i] * take

            # 0/1 knapsack for this combined item
            for capacity in range(max_weight, w - 1, -1):
                dp[capacity] = max(dp[capacity], dp[capacity - w] + v)

            remaining -= take
            k <<= 1  # Double: 1, 2, 4, 8, ...

    return dp[max_weight]


# Example
weights = [2, 3, 5]
values = [10, 15, 25]
counts = [3, 2, 1]
print(f"Bounded knapsack max: {bounded_knapsack(10, weights, values, counts)}")

Binary splitting converts O(count) items to O(log count), dramatically reducing complexity for items with large counts.

Multi-Dimensional Knapsack

Real-world problems often have multiple constraints (weight, volume, time):

def multi_dimensional_knapsack(
    capacities: list[int],
    weights: list[list[int]],
    values: list[int]
) -> int:
    """Knapsack with multiple resource constraints."""
    dims = len(capacities)
    n = len(values)

    size = tuple(c + 1 for c in capacities)
    dp = np.zeros(size, dtype=int)

    # Simplified 2D version:
    if dims == 2:
        for i in range(n):
            for w in range(capacities[0], weights[i][0] - 1, -1):
                for v in range(capacities[1], weights[i][1] - 1, -1):
                    dp[w][v] = max(dp[w][v],
                                   dp[w - weights[i][0]][v - weights[i][1]] + values[i])

    return dp[tuple(capacities)]


# 2D example: weight and volume constraints
max_weight = 10
max_volume = 15
items_weights = [[5, 3], [4, 8], [7, 4], [3, 5]]
items_values = [50, 60, 70, 30]
print(multi_dimensional_knapsack([max_weight, max_volume], items_weights, items_values))

Time Complexity: O(N * Pi(capacities)) – grows exponentially with dimensions Space: Same as time, limiting practical use to 2-3 dimensions

Subset Sum Problem

A closely related problem: determine if a subset sums to a target value:

def subset_sum(nums: list[int], target: int) -> bool:
    """Check if any subset sums to target."""
    dp = [False] * (target + 1)
    dp[0] = True

    for num in nums:
        for s in range(target, num - 1, -1):
            if dp[s - num]:
                dp[s] = True

    return dp[target]


def subset_sum_count(nums: list[int], target: int) -> int:
    """Count subsets that sum to target."""
    dp = [0] * (target + 1)
    dp[0] = 1

    for num in nums:
        for s in range(target, num - 1, -1):
            dp[s] += dp[s - num]

    return dp[target]


# Testing
nums = [3, 34, 4, 12, 5, 2]
print(f"Subset sum to 9: {subset_sum(nums, 9)}")     # True (4+5 or 3+4+2)
print(f"Subset sum to 30: {subset_sum(nums, 30)}")   # False
print(f"Subset sum count to 9: {subset_sum_count(nums, 9)}")  # 2

This is equivalent to 0/1 knapsack where each item’s weight equals its value.

Testing and Verification

import unittest

class TestKnapsack(unittest.TestCase):
    def test_01_knapsack(self):
        weights = [1, 4, 3]
        values = [1500, 3000, 2000]
        self.assertEqual(knapsack_01_2d(4, weights, values), 3500)
        self.assertEqual(Knapsack1D(4, weights, values), 3500)

    def test_unbounded(self):
        weights = [1, 3, 4]
        values = [15, 50, 60]
        self.assertEqual(unbounded_knapsack(8, weights, values), 120)

    def test_fractional(self):
        weights = [10, 20, 30]
        values = [60, 100, 120]
        self.assertAlmostEqual(fractional_knapsack(50, weights, values), 240.0)

    def test_subset_sum(self):
        self.assertTrue(subset_sum([3, 34, 4, 12, 5, 2], 9))
        self.assertFalse(subset_sum([3, 34, 4, 12, 5, 2], 30))

    def test_edge_cases(self):
        self.assertEqual(knapsack_01_2d(0, [1, 2], [10, 20]), 0)
        self.assertEqual(knapsack_01_2d(5, [], []), 0)
        self.assertEqual(unbounded_knapsack(0, [1, 2], [10, 20]), 0)

if __name__ == '__main__':
    unittest.main()

Expanded Real-World Applications

Cloud Resource Allocation

Cloud providers allocate VMs to physical servers, maximizing revenue under multiple constraints (CPU, memory, storage). Each VM has resource requirements and a price; the knapsack model ensures optimal packing. With 10+ resource dimensions, multi-dimensional knapsack variants optimize data center utilization.

Financial Portfolio Selection

The Markowitz mean-variance framework selects investments to maximize returns under risk constraints. Each asset has an expected return, risk score, and capital requirement. The knapsack formulation selects the optimal subset of investments given a fixed budget.

Cutting Stock Problem

Manufacturers cut raw materials (steel beams, paper rolls) into smaller pieces to fulfill orders while minimizing waste. This is a variant that combines knapsack with bin packing, often solved with column generation and dynamic programming.

Project Selection

Organizations select projects under budget and resource constraints. Each project has expected value and resource requirements. The knapsack model helps prioritize projects, with dependencies handled through constraint programming extensions.

Variant Comparison

Variant Item Availability Algorithm Time Complexity
0/1 Knapsack 0 or 1 of each DP (2D/1D) O(NW)
Fractional Any portion Greedy O(N log N)
Unbounded Unlimited DP (forward) O(NW)
Bounded Limited count DP + binary split O(NW log C)
Multi-D Multiple constraints Multi-dim DP O(N*Pi(D_i))
Subset Sum 0 or 1 DP (boolean) O(NT)

Resources

Comments

👍 Was this article helpful?