How to Solve Knapsack Problems (0-1, Unbounded, Subset) Using Dynamic Programming
You solve knapsack problems by defining a DP state dp[i][w] representing the maximum value achievable using the first i items with capacity w, then applying recurrence relations that either exclude the item or include it (once for 0-1 knapsack, unlimited times for unbounded knapsack), achieving optimal O(N·W) time complexity with O(W) space optimization.
The repository labuladong/fucking-algorithm provides authoritative dynamic programming solutions for classic knapsack variants in its Dynamic Programming series. The framework centers on systematic state transition logic that distinguishes between constrained single-use scenarios and flexible multi-use configurations, enabling you to maximize value within any weight capacity.
Understanding the 0-1 Knapsack Problem
According to 动态规划系列/背包问题.md, the 0-1 knapsack restricts each item to at most one selection. This constraint fundamentally shapes the state transition logic and iteration pattern required for correct computation.
State Definition and Transition
The solution defines dp[i][w] as the maximum value obtainable by selecting from the first i items without exceeding capacity w. The reference Java implementation (lines 44-63) establishes the recurrence by evaluating two exclusive choices for each item:
- Exclude item
i: Carry forward the previous optimal valuedp[i-1][w] - Include item
i(only ifw ≥ wt[i-1]): Addval[i-1]to the optimal subproblem solutiondp[i-1][w-wt[i-1]]
This yields the transition equation:
dp[i][w] = max(dp[i-1][w], dp[i-1][w-wt[i-1]] + val[i-1])
Space Optimization to O(W)
Because each state depends exclusively on the previous row (i-1), we compress the 2D DP table into a 1D array of size W+1. The critical implementation detail requires iterating backwards through the capacity dimension:
for i in range(N):
for w in range(W, wt[i] - 1, -1): # Decreasing order
dp[w] = max(dp[w], dp[w - wt[i]] + val[i])
This backward traversal prevents overwriting dp[w-wt[i]] with the current item's value before it is used to compute dp[w], ensuring each item is counted at most once.
Solving the Unbounded (Complete) Knapsack Problem
For the unbounded knapsack—where each item may be selected an unlimited number of times—the repository references the external article linked in 动态规划系列/状态压缩技巧.md (line 198). While the state definition remains conceptually similar, the transition logic modifies to allow repeated selection of the same item.
Modified State Transition
The recurrence shifts to a 1D formulation where dp[w] represents the maximum value for capacity w considering all items up to the current index:
dp[w] = max(dp[w], dp[w-wt[i]] + val[i]) for all w ≥ wt[i]
Unlike the 0-1 version, the term dp[w-wt[i]] on the right-hand side may already include the current item i, effectively permitting unlimited reuse.
Implementation Differences
The traversal direction reverses to forwards iteration:
for i in range(N):
for w in range(wt[i], W + 1): # Increasing order
dp[w] = max(dp[w], dp[w - wt[i]] + val[i])
This forward pass ensures that when computing dp[w], the subproblem dp[w-wt[i]] potentially contains the current item's value, enabling the accumulation of multiple copies.
Algorithm Comparison and Complexity Analysis
Both variants achieve identical asymptotic complexity but require distinct iteration patterns:
- 0-1 Knapsack:
O(N·W)time andO(W)space. Iterate items outer loop, capacity decreasing inner loop. Each item processed exactly once per capacity. - Unbounded Knapsack:
O(N·W)time andO(W)space. Iterate items outer loop, capacity increasing inner loop. Allows accumulation of multiple item copies.
Complete Python Implementations
The following implementations demonstrate the practical application of these DP principles:
# 0-1 knapsack: each item used at most once
def knapsack_01(W, wt, val):
N = len(wt)
dp = [0] * (W + 1)
for i in range(N):
# Traverse backwards to prevent reuse
for w in range(W, wt[i] - 1, -1):
dp[w] = max(dp[w], dp[w - wt[i]] + val[i])
return dp[W]
# Unbounded knapsack: unlimited item copies
def knapsack_unbounded(W, wt, val):
N = len(wt)
dp = [0] * (W + 1)
for i in range(N):
# Traverse forwards to enable reuse
for w in range(wt[i], W + 1):
dp[w] = max(dp[w], dp[w - wt[i]] + val[i])
return dp[W]
# Example execution
if __name__ == "__main__":
W = 4
wt = [2, 1, 3]
val = [4, 2, 3]
print("0-1 knapsack:", knapsack_01(W, wt, val)) # Output: 6
print("Unbounded knapsack:", knapsack_unbounded(W, wt, val)) # Output: 8
Summary
- State definition: Use
dp[i][w](or compresseddp[w]) to track maximum value for firstiitems and capacityw. - 0-1 transition:
max(dp[i-1][w], dp[i-1][w-wt[i-1]] + val[i-1])with backwards capacity iteration. - Unbounded transition:
max(dp[w], dp[w-wt[i]] + val[i])with forwards capacity iteration. - Space optimization: Both approaches reduce from
O(N·W)toO(W)by using 1D arrays with appropriate traversal directions. - Source reference: Implementation details are documented in
动态规划系列/背包问题.mdand linked resources withinlabuladong/fucking-algorithm.
Frequently Asked Questions
What is the fundamental difference between 0-1 and unbounded knapsack?
The 0-1 knapsack restricts each item to zero or one use, requiring you to choose between including or excluding each item exactly once. The unbounded knapsack permits unlimited copies of each item, transforming the decision from a binary choice into a quantity optimization problem where you can repeatedly select profitable items.
Why must we iterate backwards for 0-1 knapsack but forwards for unbounded knapsack?
In the 0-1 knapsack, backwards iteration ensures that dp[w-wt[i]] refers to the state from the previous item iteration (i-1), preventing the current item from being counted multiple times in the same solution. For unbounded knapsack, forwards iteration allows dp[w-wt[i]] to potentially contain the current item already, enabling the accumulation of unlimited copies as you progress through increasing capacities.
Can the subset sum problem be solved using this knapsack framework?
Yes, the subset sum problem is a special case of the 0-1 knapsack where val[i] = wt[i] and the goal is to determine if capacity W can be filled exactly. You modify the DP to track boolean feasibility (dp[w] = dp[w] or dp[w-wt[i]]) rather than maximizing value, utilizing the same state transition logic described in 动态规划系列/背包问题.md.
How does the space optimization from 2D to 1D DP maintain correctness?
The 2D array dp[i][w] only depends on row i-1, never on earlier rows. By overwriting a single 1D array dp[w] in-place, we preserve the necessary previous state values if we traverse in the correct order: backwards for 0-1 (preserving the i-1 state for smaller capacities) and forwards for unbounded (allowing the updated state to influence larger capacities immediately).
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →