Knapsack Problem Variants

Master the 0/1, unbounded, and fractional knapsack variants with DP solutions and optimization techniques for resource allocation problems.

published: reading time: 17 min read author: GeekWorkBench

Knapsack Problem Variants

The knapsack problem asks: given items with weights and values, maximize total value while respecting a weight capacity. This sounds simple, but it shows up everywhere — budget allocation, cargo loading, portfolio selection, job scheduling. 0/1 knapsack is NP-hard, but DP gives you pseudo-polynomial O(nW) solutions that work well in practice when weights aren’t enormous.

Three variants matter: 0/1 knapsack (discrete items, take-or-leave decisions), unbounded knapsack (each item can be taken unlimited times), and fractional knapsack (items can be divided, solvable greedily). Interview problems routinely mix these variants or add constraints like “exactly fill the capacity” or “minimize wasted space.” The state transition is the key: for 0/1, you decide whether to include each item based on what you already computed for smaller capacities; for unbounded, you can reuse items.

Introduction

The knapsack problem asks: given a set of items with weights and values, maximize total value while respecting a weight capacity. This deceptively simple optimization underlies real-world resource allocation—from budget allocation and cargo shipping to portfolio selection and job scheduling. Despite being NP-hard for 0/1 knapsack, dynamic programming provides pseudo-polynomial O(nW) solutions that work well in practice when weights aren’t enormously large.

Three variants form the core vocabulary: 0/1 knapsack (discrete items, take-or-leave decisions), unbounded knapsack (each item can be taken unlimited times), and fractional knapsack (items can be divided, solvable greedily). Interview problems routinely mix these variants or add constraints like “exactly fill the capacity” or “minimize wasted space.” The state transition is the key: for 0/1, you decide whether to include each item based on what you already computed for smaller capacities; for unbounded, you can reuse items.

This guide covers implementation patterns for all three variants, the space optimization from O(nW) to O(W), and how to recognize when a problem is knapsack in disguise. You’ll learn the difference between the “complete” knapsack and “0/1 knapsack” formulations, how to handle multi-dimensional constraints, and practical tips for when the pseudo-polynomial DP is too slow (approximation schemes or integer programming for large weights).

When to Use

Knapsack DP applies when:

  • Resource allocation with constraints — Budget limits, weight limits, capacity limits
  • Selection with trade-offs — Every choice has a cost and benefit
  • Cargo loading problems — Trucks, ships, containers with weight limits
  • Investment selection — Choosing investments with capital constraints
  • Task scheduling — Selecting tasks with time/resource requirements

When Not to Use

  • Fractional items and unbounded choice — Use greedy (sort by value/weight ratio)
  • Massive weight capacities — O(nW) becomes impractical; use approximation algorithms
  • Truly continuous values — When decisions aren’t discrete, LP relaxation works better

Architecture: DP State Transitions

graph TD
    A["dp[i][w] = max value using first i items with weight limit w"] --> B{"Item i taken?"}
    B -->|No| C["dp[i-1][w]"]
    B -->|Yes| D["dp[i-1][w-weight[i]] + value[i]"]

    A2[Unbounded: unlimited copies] --> B2{"Item i taken?"}
    B2 -->|No| C2["dp[i-1][w]"]
    B2 -->|Yes| D2["dp[i][w-weight[i]] + value[i]"]

Implementation

0/1 Knapsack (Standard)

def knapsack_01(weights, values, capacity):
    """
    0/1 knapsack: each item taken at most once.
    Time: O(n * capacity), Space: O(capacity)
    """
    n = len(weights)
    # Space-optimized: only need previous row
    dp = [0] * (capacity + 1)

    for i in range(n):
        # Traverse backwards to avoid using updated values
        for w in range(capacity, weights[i] - 1, -1):
            dp[w] = max(dp[w], dp[w - weights[i]] + values[i])

    return dp[capacity]

Unbounded Knapsack

def knapsack_unbounded(weights, values, capacity):
    """
    Unbounded knapsack: unlimited copies of each item.
    Time: O(n * capacity), Space: O(capacity)
    """
    dp = [0] * (capacity + 1)

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

    return dp[capacity]

Complete (Unbounded) with Item Tracking

def knapsack_with_reconstruction(weights, values, capacity):
    """
    0/1 knapsack with item selection reconstruction.
    """
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]

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

    # Reconstruct solution
    selected = []
    w = capacity
    for i in range(n, 0, -1):
        if dp[i][w] != dp[i - 1][w]:
            selected.append(i - 1)
            w -= weights[i - 1]

    return dp[n][capacity], selected

Multi-Dimensional Knapsack

def knapsack_2d(weights, costs, values, max_weight, max_cost):
    """
    0/1 knapsack with two constraints (weight and cost).
    Time: O(n * max_weight * max_cost)
    """
    dp = [[0] * (max_cost + 1) for _ in range(max_weight + 1)]

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

    return dp[max_weight][max_cost]

Common Pitfalls / Anti-Patterns

  1. Forgetting backward traversal in 0/1 — Forward traversal uses updated values (items counted twice)
  2. Mutable default arguments — Never use dp={} in function signatures
  3. Confusing unbounded with 0/1 — Unbounded traverses forward, uses dp[w - weight[i]] (same row)
  4. Integer overflow — Use appropriate numeric types for large capacity values

Real-world Failure Scenarios

ScenarioWhat Went WrongLesson Learned
E-commerce inventory systemO(nW) DP with capacity 1M caused OOM on 4GB serverWeight ranges often create sparse solution spaces; consider memoization or meet-in-the-middle
Container loading optimizationNP-hard 0/1 approximated with greedy, missing 23% valueWhen precision matters, use branch-and-bound or ILP solvers for small item counts
Portfolio selection with transaction costsSimple knapsack ignored fixed costs per transactionAdd transaction cost as separate dimension or post-process to group small items
Job scheduling with deadlinesTreated as 0/1 but jobs had soft dependenciesPrecedence constraints require DAG-based DP, not standard knapsack

Trade-off Analysis

VariantTime ComplexitySpace ComplexityOptimalityUse When
0/1 Knapsack (2D table)O(nW)O(nW)Always optimalNeed to reconstruct selected items
0/1 Knapsack (1D optimized)O(nW)O(W)Always optimalOnly need max value, W is manageable
UnboundedO(nW)O(W)Always optimalInfinite supply of items
FractionalO(n log n)O(1)Always optimalItems divisible, greedy works
Multi-Dimensional 0/1O(nW*C)O(W*C)Always optimalTwo+ constraints (weight + cost + …)
Approximate (PTAS)O(n^(1/ε))O(n)(1-ε)-optimalLarge W, need provable bounds
Greedy ApproximateO(n log n)O(1)No guaranteeFast, acceptable to miss optimal

When to choose approximation: If W > 10^7 and n > 500, O(nW) becomes impractical. Greedy gives ~50% of optimal in practice. PTAS gives (1-ε) guarantee with O(n^(1/ε)) time.

Quick Recap Checklist

  • 0/1 knapsack: traverse backwards, use previous row only
  • Unbounded: traverse forward, can use current row (infinite supply)
  • State: dp[i][w] = max value using first i items with weight limit w
  • Base case: dp[0][w] = 0 (no items) and dp[i][0] = 0 (no capacity)
  • Time complexity O(n × capacity) — pseudo-polynomial

Interview Questions

Why must you traverse backwards in 0/1 knapsack?

Backward traversal is the fix for a subtle bug: when you update dp[w] going forward, the item i you're processing could get counted again in the same iteration. Going backward, dp[w - weight[i]] is guaranteed to still hold the value from item i-1, so you never double-count.

How do you reduce space complexity from O(nW) to O(W)?

The 1D optimization works because dp[w] only ever reads from dp[w - weight[i]]. After you update dp[w] for item i, you never need the old dp[w] again in the same item's iteration—it's consumed. Backward traversal ensures that when you read dp[w - weight[i]], you haven't overwritten it yet with item i's contribution. This collapses the 2D table to a single row without losing correctness.

Why is fractional knapsack solvable greedily but 0/1 is not?

Fractional knapsack has the greedy-choice property: picking the item with the highest value-to-weight ratio always leads to an optimal solution. You can cut it, so filling your capacity with the best ratio items first never backfires. 0/1 knapsack doesn't have this property—grabbing a high-ratio item might prevent you from taking several medium-ratio items that together are worth more. This missing optimal substructure is exactly what makes 0/1 NP-hard.

How would you modify 0/1 knapsack to handle items with both weight and volume constraints?

Multi-dimensional knapsack adds a second constraint (volume, cost, time—whatever matters for your problem). You expand the DP table to track both: dp[w][v] holds the max value achievable with weight w and volume v. The transition checks both limits before adding an item. Complexity multiplies: O(n × maxWeight × maxVolume). This shows up in container loading, vehicle routing with fuel constraints, and portfolio selection with transaction costs.

What is the meet-in-the-middle technique for knapsack and when does it help?

Split the items in half—call them left and right. Enumerate every subset sum for each half (2^(n/2) each, which is manageable up to about n=40). Then combine them: for each subset sum from the left, find the best matching sum from the right that fits within the capacity. This turns O(2^n) brute force into O(2^(n/2)), which is the difference between waiting forever and getting an answer before your interview ends.

How does bounded knapsack differ from 0/1 when items have limited quantities?

Bounded knapsack specifies how many copies of each item you can take (e.g., item i has quantity limit m[i]). The state transition adds a third dimension: dp[i][w] = max(dp[i-1][w], dp[i-1][w - k*weight[i]] + k*value[i]) for k = 1 to min(m[i], w/weight[i]). A common optimization uses binary decomposition: split each item into powers-of-two bundles (1, 2, 4, ..., remainder) and treat each bundle as a separate 0/1 item, reducing bounded to O(n log max_quantity) items.

What is the relationship between subset sum and 0/1 knapsack?

Subset sum is a special case of 0/1 knapsack where all values equal weights (or values are the targets themselves). The recurrence dp[w] = dp[w] or dp[w - num] mirrors knapsack with unit values. Conversely, any 0/1 knapsack can be reduced to subset sum by treating (value - weight) pairs as numbers and looking for a target sum, though the transformation loses the optimization objective. Subset sum being NP-complete confirms 0/1 knapsack is NP-hard.

When would you prefer the full 2D DP table over space-optimized 1D array for 0/1 knapsack?

The 2D table dp[i][w] is necessary when you need to reconstruct which items were selected. The space-optimized version discards which items contributed to each state. Also, the 2D version can be easier to debug since each row represents a clear "first i items" boundary. Use 1D only when you need the final maximum value and don't care about the actual selection.

How do you initialize the DP table and why does the base case matter?

Base cases: dp[0][w] = 0 (zero items can produce zero value) and dp[i][0] = 0 (zero capacity holds zero value). These anchor the recurrence. All other cells start at 0 because we're maximizing and the minimum achievable value is 0 (no items selected). Incorrect initialization—like starting at negative infinity—would corrupt the recurrence. The base case also reveals why negative weights require special handling: they break the natural ordering of capacity.

How would you adapt 0/1 knapsack for a multi-objective variant that maximizes value while minimizing weight?

Use a 2D DP table where dp[i][v] = minimum weight needed to achieve at least value v using first i items. Traverse values in ascending order and stop when no feasible solution exists (weight exceeds capacity). Alternatively, use Pareto optimality: track all non-dominated pairs (value, weight) and prune any solution strictly worse in both dimensions. This produces a Pareto frontier rather than a single solution.

What is the difference between memoization (top-down) and tabulation (bottom-up) for knapsack?

Tabulation builds the DP table iteratively from smaller subproblems up to the full solution—fast but processes all states even if unused. Memoization (top-down) recursively computes only states actually reached, lazily filling the table. Memoization avoids wasted work on sparse state spaces but has recursion overhead. For knapsack, tabulation is usually faster due to no function call overhead; memoization shines when the reachable state space is much smaller than the full n x W grid.

How do you handle the case where item weights or capacities are very large (causing integer overflow)?

Use arbitrary-precision integers (Python's int handles this natively). In languages like C++ or Java, use BigInteger or BigDecimal. Alternatively, normalize values: if weights are large but limited in range, use dictionary-based sparse representation (hash maps instead of arrays) to store only reachable states. A third approach: scale weights to smaller units or use modular arithmetic if the problem structure permits.

Can 0/1 knapsack be solved with a greedy heuristic? When might a greedy approach be acceptable?

No greedy algorithm guarantees the optimal 0/1 knapsack solution. A common heuristic sorts by value-to-weight ratio (v_i/w_i) and picks items until capacity is full—it runs in O(n log n) but can be arbitrarily far from optimal. Acceptable when: (1) speed matters more than precision, (2) items are small and uniformly distributed, (3) you need a quick bound for branch-and-bound. The greedy solution also serves as a warm start for iterative improvement.

How would you solve knapsack when weights can be negative?

Negative weights break standard DP assumptions because adding a negative weight could increase remaining capacity, creating cycles. Solutions: (1) Shift all weights to positive by adding a constant, then adjust capacity; (2) Use unconstrained DP with a hash map keyed by actual weight sum, tracking max value for each reachable weight; (3) If only some weights are negative, treat them separately—negative-weight items should always be taken if valuable (unbounded effect) and require special handling in the recurrence. The problem becomes strongly NP-hard even with a single negative weight.

What is the branch and bound approach for knapsack and how does it improve over brute force?

Branch and bound explores the item decision tree but prunes branches that cannot beat the current best solution. A bound is computed using the fractional knapsack relaxation (upper bound = current value + remaining capacity × best value-per-weight among undecided items). The algorithm: (1) Sort items by density descending; (2) Recursively decide to include/exclude each item; (3) Bound: if fractional upper bound ≤ best known integer solution, prune. For n=30-50 items this can solve problems that brute force O(2^n) cannot, though worst case is still exponential.

How does the "multiple knapsack" problem differ and what are the common approaches?

Multiple knapsack assigns each item to at most one of K knapsacks with individual capacities W_1...W_k. The straightforward approach adds a dimension: dp[i][w1][w2]...[wk]—exponential in K, quickly intractable. Practical approaches: (1) Merge knapsacks into one super-capacity if items are fungible; (2) Use integer linear programming (ILP) solvers for exact solutions; (3) Apply greedy decomposition—assign items to the knapsack with most remaining capacity—then locally optimize with exchanges between knapsacks.

Why is O(nW) called pseudo-polynomial and what are the implications for very large W?

O(nW) is not polynomial in the input size because W is represented in binary, requiring log(W) bits. A truly polynomial algorithm would be O(n^c) where c is constant. For W = 2^100 (astronomical), even n=1 gives 2^100 operations. This matters for cryptography (large key spaces) and any problem where capacity grows exponentially with input encoding. Mitigation: approximation schemes (PTAS), integer linear programming, or heuristic methods when W is large relative to n.

How do you handle a knapsack variant where the capacity changes over time (dynamic knapsack)?

Dynamic capacity arises in problems like budget cycles or cargo with refueling stops. Two approaches: (1) Expand the state to include time/capacity dimension: dp[i][w][t] where t is the time step and capacity varies deterministically (e.g., decays or resets); (2) Treat each time period as a separate knapsack subproblem and use the solution as input for the next period. If capacity changes stochastically, use Markov decision process (MDP) formulation with value iteration. The complexity grows multiplicatively with time steps.

What is the difference between the FPTAS (Fully Polynomial Time Approximation Scheme) and the standard PTAS for knapsack?

Both guarantee a (1-ε) optimal solution. PTAS runs in O(n^(1/ε))—polynomial in n but exponential in 1/ε. FPTAS runs in O(n^3 / ε) or O(n^2 / ε)—truly polynomial in both n and 1/ε. The FPTAS for knapsack works by scaling values: divide all values by a factor that rounds them to O(n/ε) distinct levels, then solve the rounded instance with DP. The rounding introduces at most (1-ε) error. FPTAS is preferred when you need a provable bound with scalable precision.

How would you modify knapsack to handle items with dependencies (e.g., item B requires item A to be taken)?

Item dependencies form a constraint graph. Solutions: (1) If dependencies form a DAG, perform topological sort and use grouped DP where each strongly connected component is collapsed into a "composite item" with all valid internal combinations precomputed; (2) Add an "include flag" for each dependency pair: treat (A, B) as a combined item with weight = w_A + w_B and value = v_A + v_B; (3) Use the inclusion-exclusion principle for OR constraints where taking B requires either A or C (A ∪ C). For complex dependency graphs, consider ILP formulation which natively expresses logical constraints.

Further Reading

Conclusion

You should now be able to recognize knapsack patterns in interview problems and pick the right variant. For 0/1 knapsack, remember backward traversal to avoid double-counting items. For unbounded, forward traversal lets you reuse the same item. When you see a greedy choice opportunity (fractional items), sort by value-to-weight ratio and grab what fits. If you need to reconstruct which items were selected, keep the full DP table. For more on dynamic programming patterns, see Dynamic Programming: Introduction.

Category

Related Posts

Memoization vs Tabulation: Two Faces of Dynamic Programming

Understand the two fundamental DP approaches—top-down with memoization and bottom-up with tabulation—plus hybrid techniques like the/m on the fly.

#dynamic-programming #memoization #tabulation

1D Dynamic Programming Problems: Classic Patterns

Learn to solve common 1D dynamic programming problems including climbing stairs, house robber, and coin change with optimized space solutions.

#dynamic-programming #dp #1d-dp

2D Dynamic Programming: Matrix Chain and Beyond

Master 2D DP problems with two state variables for string manipulation, matrix chain multiplication, and optimal game strategies.

#dynamic-programming #dp #2d-dp