Hello! Welcome back to our module on dynamic programming.
In our last lesson, we explored 2D DP problems like Longest Common Subsequence and Edit Distance, where the state dp[i][j] helped us solve problems comparing two different sequences. Today, we're going to tackle another major DP pattern that is a favorite in technical interviews: Knapsack problems.
This lesson will teach you how to solve problems where you need to make choices to maximize value under a given constraint. This is a powerful pattern seen in resource allocation, investment choices, and more. We will focus on the two most common variations you'll encounter:
- 0/1 Knapsack: You can either take an item or leave it. No duplicates.
- Unbounded Knapsack: You have an infinite supply of each item.
Mastering this pattern is excellent preparation for your interviews, as it tests your ability to define a DP state, formulate a recurrence, and apply clever optimizations.
1. The 0/1 Knapsack Problem
Imagine you're a thief with a knapsack that can hold a maximum weight, say W. You enter a house with N items, each with a specific weight and value. Your goal is to choose which items to steal to maximize the total value, without exceeding your knapsack's weight limit. In the "0/1" version, for each item, you have two choices: take it (1) or don't take it (0). You cannot take a fraction of an item or take the same item multiple times.
Why a Greedy Approach Fails
Your first instinct might be a greedy one: "I'll just take the most valuable items first!" or "I'll take items with the best value-to-weight ratio!" While simple, these strategies don't guarantee the optimal solution. You might fill your bag with a high-value, heavy item, leaving no room for several lighter items that, combined, would have yielded a greater total value.
To see a concrete example of greedy failing, let's turn to a video that introduces the problem and this specific pitfall.
DP 19. 0/1 Knapsack | Recursion to Single Array Space Optimised Approach | DP on Subsequences
The video 'DP 19. 0/1 Knapsack' from the channel 'take U forward' provides a great introduction to the problem. The first few minutes clearly explain the problem statement and demonstrate why a simple greedy approach is insufficient.
Watch from the beginning until 07:57. Focus on: 00:00 - 04:23: The initial problem setup using the thief analogy. 04:23 - 07:57: The example that shows why a greedy strategy (picking the most valuable item) leads to a suboptimal result. This justifies the need for dynamic programming.
The Dynamic Programming Solution Path
Since greedy fails, we must explore all valid combinations and find the best one. This is a perfect use case for DP. We'll follow a standard progression: from a simple (but slow) recursive solution to a highly optimized one.
Step 1: Recursive Formulation (Top-Down)
The core of any DP solution is defining a function that solves a subproblem.
- State: Let's define
f(index, capacity)as the maximum value we can obtain using items from0toindexwith a knapsack of the givencapacity. - Choices: At any
index, we have two choices for the item:- Not Take: We skip the item at
index. The value is whatever we can get from the remaining items:f(index - 1, capacity). - Take: We put the item at
indexin our knapsack. The value isvalue[index]plus whatever we can get from the remaining items with the reduced capacity:value[index] + f(index - 1, capacity - weight[index]). This choice is only possible ifweight[index] <= capacity.
- Not Take: We skip the item at
- Recurrence Relation: We want to maximize our value, so we take the best of these two choices:
- Base Case: If
indexis 0, we can only take that first item if its weight is less than or equal to the capacity. Ifcapacityis 0 or we run out of items, we can't add any more value.
Step 2: Memoization, Tabulation, and Space Optimization
The pure recursive solution is exponential because it recomputes the same subproblems (e.g., f(5, 10)) many times. We can optimize this using DP. The following video resource will guide you through the entire process, from recursion to a final, space-optimized solution.
DP 19. 0/1 Knapsack | Recursion to Single Array Space Optimised Approach | DP on Subsequences
Continuing with the same 'take U forward' video, let's see how this recursive idea is transformed into an efficient DP solution. This is a masterclass in DP optimization.
Watch from 07:57 to 41:06. This is the core of the lesson. Pay close attention to each step: 07:57 - 15:27: Understanding the recursive structure ('pick' vs 'not pick'). 15:27 - 24:15: How recursion leads to overlapping subproblems and how memoization (top-down DP) with a 2D array dp[index][capacity] solves this, bringing the complexity to O(N * W). 24:15 - 27:44: Converting the solution to tabulation (bottom-up DP), which eliminates recursion stack space. 32:18 - 41:06: The crucial space optimization steps. First to O(2 * W) using two rows, and finally to O(W) using a single row. Notice why the inner loop must run backward for this to work correctly. This is a key interview insight.
Summary of 0/1 Knapsack DP:
- Time Complexity: O(N * W), where N is the number of items and W is the capacity.
- Space Complexity:
- Naive Tabulation: O(N * W)
- Optimized: O(W)
The key takeaway for the O(W) space optimization is that when calculating the values for the current item i, we need the results from the previous item i-1. By iterating the capacity w from W down to 0, we ensure that when we compute dp[w], the value dp[w - weight[i]] is still from the previous iteration (row i-1), not the one we just updated in this iteration.
Test your understanding!
Consider the recurrence for the tabulated 0/1 knapsack:dp[i][w] = max(dp[i-1][w], value[i-1] + dp[i-1][w - weight[i-1]])
If we optimize to a single array dp[w], why would iterating w from 0 to W (a forward loop) give the wrong answer?
Show answer
If you iterate forward, when you calculate dp[w], the value dp[w - weight[i-1]] might have already been updated in the current i-th iteration. This means you are effectively allowing the same item i-1 to be used multiple times to fill the capacity w, which changes the problem from 0/1 Knapsack to Unbounded Knapsack. The backward loop prevents this by preserving the previous row's values until they are used.
2. The Unbounded Knapsack Problem
The Unbounded Knapsack problem is a variation where you have an infinite supply of each item. You can pick any item as many times as you want, as long as you stay within the weight limit W.

Adapting the Recurrence Relation
The logic is almost identical to 0/1 Knapsack. The only difference is in the "Take" choice.
- State:
f(index, capacity)remains the same. - Choices:
- Not Take: Same as before. We move to the next item type.
f(index - 1, capacity). - Take: We take the item at
index. The value isvalue[index]plus the best we can do with the remaining capacity. Since we have an infinite supply, we can potentially take this same item again. Therefore, we stay at the sameindex:value[index] + f(index, capacity - weight[index]).
- Not Take: Same as before. We move to the next item type.
This single change—f(index, ...) instead of f(index - 1, ...) in the "Take" case—is the only modification needed.
For a detailed walkthrough, let's refer to another excellent video.
DP 23. Unbounded Knapsack | 1-D Array Space Optimised Approach
The video 'DP 23. Unbounded Knapsack' from 'take U forward' explains this variation. It builds directly on the concepts from the 0/1 Knapsack video.
Watch the selected parts of this video: 00:00 - 03:41: Introduction to the problem and how it differs from 0/1 Knapsack (infinite supply). 03:41 - 09:31: Formulating the recurrence relation. Focus on the key change: when you 'take' an item, you stay at the same index. 16:44 - 22:23: The space optimization. Notice how the small change in the recurrence allows for a 1D array solution with a forward loop for capacity. This beautifully contrasts with the 0/1 Knapsack optimization.
Space Optimization for Unbounded Knapsack
As highlighted in the video and our test question above, the recurrence for Unbounded Knapsack is:dp[i][w] = max(dp[i-1][w], value[i-1] + dp[i][w - weight[i-1]])
Notice the dp[i] on the right side of the max function. This means that to calculate the solution for the current item i and capacity w, we use results from the previous item (dp[i-1][w]) and results from the current item at a smaller capacity (dp[i][w - weight[i-1]]).
When we optimize to a single O(W) array, this translates perfectly to a forward loop (i.e., w from 0 to W). When we compute dp[w], we want to use the already-updated dp[w - weight[i-1]] from the current iteration, because that correctly models taking the same item more than once.
A clean Python implementation is also available in the article "Mastering the Unbounded Knapsack Problem" from AlgoCademy. You can see how the logic translates to a very concise 1D DP solution.
Mastering the Unbounded Knapsack Problem
For a concise Python implementation and a textual summary, review this article.
Focus on the 'Implementing the Solution' section to see the 1D array bottom-up DP approach in Python. The nested loops directly implement the logic we've discussed.
Conclusion
In this lesson, you've learned one of the most important dynamic programming patterns for coding interviews. The Knapsack problem family demonstrates how to make optimal choices under constraints.
Key Takeaways:
- Knapsack Pattern: Based on a "take" vs. "not take" choice for each item to maximize a value.
- State: The DP state is typically
dp[index][capacity]. - 0/1 vs. Unbounded: The key difference is in the "Take" recursion:
- 0/1: Move to the next item:
f(index - 1, ...) - Unbounded: Stay at the same item:
f(index, ...)
- 0/1: Move to the next item:
- Space Optimization (O(W)): This difference leads to a crucial implementation detail:
- 0/1 Knapsack: Requires a backward capacity loop (
Wto0) to use results from the previous row. - Unbounded Knapsack: Uses a forward capacity loop (
0toW) to use results from the current row.
- 0/1 Knapsack: Requires a backward capacity loop (
Understanding these nuances is a strong signal in an interview that you have a deep grasp of dynamic programming.
Preview of the Next Lesson:
We will continue with this pattern in our next lesson, "Solve coin change problems using dynamic programming." The Coin Change 2 problem ("how many combinations of coins make a certain amount?") is a classic application of the Unbounded Knapsack pattern. This will give you more practice and help solidify your understanding.