The Knapsack Problem: From 0/1 to Advanced
The Knapsack problem is the “Hello World” of Dynamic Programming. But beyond the basic 0/1 version lies a world of clever optimizations and mathematical tricks. In this post, we’ll break down the three major types and the patterns that solve them.
1. The Foundation: 0/1 Knapsack
The Rule: You have one of each item. You either take it (1) or leave it (0).
The Logic
We define dp[i][j] as the maximum value using a subset of the first i items with a total capacity j.
The choice is simple:
-
Exclude the current item:
dp[i-1][j] -
Include the current item:
dp[i-1][j - w_i] + v_i
The “Space” Pro-Tip
You don’t need a 2D array. By iterating backwards through the capacity, you can use a single 1D array.
Just update backward to avoid using the same item twice.
for item in items:
for j in range(W, item.w - 1, -1):
dp[j] = max(dp[j], dp[j - item.w] + item.v)
2. The Buffet: Complete Knapsack
The Rule: Items are infinite. Take as many as you want.
The Transformation
If you can take an item multiple times, the state dp\[j\] should depend on the current item’s updated state, not the previous one.
The Simple Twist
The code is almost identical to 0/1 Knapsack, but we iterate forwards.
for item in items:
for j in range(item.w, W + 1):
dp[j] = max(dp[j], dp[j - item.w] + item.v)
3. The Constraint: Bounded Knapsack
The Rule: Each item has a specific limit .
Approach A: Binary Splitting (The Clever Way)
A straightforward approach is to treat each item as nin_i different items, each item has the same price and value. But it significantly slows down the algorithm.
So instead of treating it as nin_i individual items, we split nin_i into powers of 2.
For example, if you have 13 items, split them into packs of 1, 2, 4, and 6. (Note that they should add up to nin_i.) These four “new” items can combine to form any number from 1 to 13.
- Complexity:
Approach B: Monotonic Queue (The Ultimate Way)
For tight time limits, we use a Monotonic Queue. We group capacities by . For each group, we find the maximum value in a “sliding window” of size .
- Complexity: — the gold standard.
Detailed implementation
1. The Core Observation
For an item with weight , value , and count , notice that a capacity can only be updated by capacities like (where ).
This means the capacities are partitioned into disjoint sets based on their remainder when divided by .
-
Set 0:
-
Set 1:
-
Set :
The algorithm processes each remainder independently.
2. The Sliding Window Transformation
Let’s look at the transition for a specific remainder . Let . The state looks like this:
To make this look like a standard “sliding window maximum” problem, we rearrange the terms:
Now, as increases, we are looking for the maximum value of in a window of size .
3. Using the Monotonic Queue
A monotonic queue is a deque (double-ended queue) that keeps elements in decreasing order.
-
Maintain the Window: As we move to the next , we remove indices from the front of the queue that are “out of date” (older than ).
-
Maintain Monotonicity: Before adding the new value to the back, we remove all values smaller than it. Why? Because a newer, larger value is always a better candidate for the future than an older, smaller value.
-
Get the Max: The current maximum for the window is always at the front of the queue.
Summary Table
| Problem Type | Core Idea | Best Complexity |
|---|---|---|
| 0/1 | Each item used once | O(NW) |
| Complete | Infinite items, forward loop | O(NW) |
| Bounded | Finite count, Binary Splitting | |
| Bounded (Opt) | Monotonic Queue | O(NW) |
Final Thoughts
The Knapsack problem isn’t just about bags and gold; it’s about resource allocation. Whether you’re optimizing server load or a financial portfolio, these DP patterns are your best friends.