Hello! Welcome back to our module on Dynamic Programming in Practice.
In our last lesson, we established a framework for analyzing the time and space complexity of DP solutions. We even used the 0/1 Knapsack problem as a case study, identifying its complexity as . Today, we will bridge the gap between that theoretical analysis and practical implementation.
This lesson is dedicated to fully solving the 0/1 Knapsack problem. You will learn how to formulate the problem's core logic, implement it using both tabulation (bottom-up) and memoization (top-down) DP approaches, and, crucially, reconstruct the solution to identify which items to select. This is a foundational problem, and mastering it is a significant step toward developing the architectural mindset you're aiming for, as its principles apply to a wide range of optimization and resource allocation challenges.
1. Understanding the 0/1 Knapsack Problem
First, let's formally define the problem. You are given a set of items, each with a specific weight and profit. You also have a knapsack with a maximum weight capacity. Your goal is to choose a subset of items to put into the knapsack such that the total profit is maximized, without exceeding the knapsack's weight capacity.
The "0/1" part means that for each item, you have two choices: either take the whole item (1) or leave it behind (0). You cannot take a fraction of an item.

As we discussed, this is a classic optimization problem that exhibits both optimal substructure and overlapping subproblems, making it a perfect candidate for dynamic programming.
To see this clearly, let's first consider a brute-force recursive approach. For each item, we can either include it or exclude it. This leads to a natural recursive structure.
The following article explains the brute-force recursive solution, which forms the basis of our DP approach. Understanding this is key to grasping the core recurrence relation.
Read the section titled 'Brute force approach using recursion'. Focus on the two cases discussed: including the nth item and excluding it. Pay special attention to the recursion tree diagram, which vividly illustrates the 'overlapping subproblems' that DP is designed to solve.
The key recurrence relation derived from this is:knapsack(i, C) = maximum profit considering items from 1 to i with capacity C.
To calculate knapsack(i, C), we decide on the i-th item:
- Exclude item
i: The profit is simplyknapsack(i-1, C). - Include item
i(ifweight[i] <= C): The profit isprofit[i] + knapsack(i-1, C - weight[i]).
The solution is the maximum of these two choices. As the article shows, this leads to an O(2^n) complexity due to re-computing the same subproblems. Now, let's use DP to optimize this.
2. Solving with Tabulation (Bottom-Up)
The tabulation method solves the problem iteratively by building a 2D table (or matrix) that stores the solutions to subproblems. Let's define dp[i][w] as the maximum profit achievable using a subset of the first i items with a knapsack capacity of w.
Our goal is to fill a table of size (n+1) x (C+1), where n is the number of items and C is the total capacity. The final answer will be in dp[n][C].
The recurrence relation for filling the table is directly derived from the recursive one:
- If the weight of the
i-th item (W[i-1]) is greater than the current capacityw, we cannot include it. So, the solution is the same as the one without this item: - Otherwise, we take the maximum of either excluding or including the item:
The first term, dp[i-1][w], represents the profit from excluding item i. The second term represents the profit from including it.
4.5 0/1 Knapsack - Two Methods - Dynamic Programming
This video provides an excellent, step-by-step walkthrough of filling the DP table using the tabulation method. The presenter's intuitive explanation is very helpful for visualizing the process.
Watch the segment from 04:12 to 16:44. Follow along as the table is constructed for a specific example. Note how each cell's value is derived from previously computed values, which is the essence of bottom-up DP.
After watching the video, you should have a clear picture of how the dp table is systematically filled, starting from the base cases (0 items or 0 capacity) and building up to the final solution.
3. Reconstructing the Solution
The dp[n][C] cell gives us the maximum possible profit, but it doesn't tell us which items we chose. To find the items, we need to backtrack through the filled table, starting from the final cell dp[n][C].
Now that you have the table, let's learn how to read it to find the selected items. This article provides a clear, step-by-step guide for this backtracking process.
Read the section 'Which Items Gives Us The Highest Value?'. Focus on the logic of comparing tab[i][w] with tab[i-1][w] to determine if an item was included.
The logic for backtracking from dp[i][w] is as follows:
- Start at
i = nandw = C. - If
dp[i][w]is the same asdp[i-1][w], it means itemiwas not included. We move up to the previous row by settingi = i - 1. - If
dp[i][w]is different fromdp[i-1][w], it means itemiwas included. We add this item to our solution set. We then move up to the previous row (i = i - 1) and decrease the capacity by the weight of the included item (w = w - weight[i-1]). - Repeat this process until
iorwbecomes 0.
4. Solving with Memoization (Top-Down)
An alternative to tabulation is memoization, which is a top-down approach. Here, we use the same recursive structure as the brute-force solution but store the result of each subproblem in a cache (often a 2D array or a hash map) the first time we compute it. Subsequent calls for the same subproblem will fetch the result directly from the cache, avoiding redundant work.
0/1 Knapsack Problem Top Down Dynamic Programming
This video is dedicated to the top-down memoized approach. It clearly illustrates the recursive calls as a tree and shows how memoization prunes the redundant branches.
Watch this video from the beginning until 15:26. Pay attention to how the recursive function checks a cache before making further calls. The tree visualization (01:25 onwards) is particularly useful for comparing this to the unoptimized recursion we saw earlier.
The memoized approach can feel more natural if you think recursively, as the code structure closely mirrors the problem's definition. The underlying logic and complexity are the same as the tabulation method, but the implementation strategy is different.
Test your understanding!
Given the following items and a knapsack capacity of 7:
- Item 1: Weight=3, Profit=40
- Item 2: Weight=4, Profit=50
- Item 3: Weight=5, Profit=60
You are filling a DP table dp[i][w]. What is the value of dp[2][7]? (i.e., max profit using the first two items with capacity 7).
Show answer
To calculate dp[2][7], we consider Item 2 (Weight=4, Profit=50). The capacity is 7.
- Exclude Item 2: The profit would be
dp[1][7]. With only Item 1 (W=3, P=40) and capacity 7, the profit is 40. So,dp[1][7] = 40. - Include Item 2: The profit would be
profit_item_2 + dp[1][7 - weight_item_2], which is50 + dp[1][3]. With only Item 1 (W=3, P=40) and capacity 3, the profit is 40. So,50 + 40 = 90.
We take the maximum of these two options: max(40, 90) = 90.
So, dp[2][7] = 90.
5. Complexity and Optimization
As reviewed in the last lesson, both the tabulation and memoization approaches have the same complexities:
- Time Complexity: , because we must solve for each unique subproblem, and there are
n * Cof them. The work per subproblem is . - Space Complexity: to store the DP table or memoization cache.
This is a pseudo-polynomial time complexity. It's efficient when the capacity C is reasonably small.
For an architect, it's also important to consider space-time trade-offs. Can we do better on space? Yes. Notice that to compute row i of the DP table, we only need information from row i-1. We don't need all n rows at once. This insight allows for a powerful optimization.
The article we looked at earlier also discusses two space optimization techniques. The final one, using a 1D array, is particularly clever and a great example of thinking about resource constraints.
Read the final two sections: 'Space-optimized version of the bottom-approach' and 'Another space-optimized version using a 1-D array'. Focus on understanding the logic of why a 1D array works when the inner loop iterates from right to left.
By using a 1D array of size C+1, we can reduce the space complexity to while keeping the time complexity at . This is a common and valuable optimization pattern in DP problems. However, this optimization makes it impossible to backtrack and find the chosen items, as we've overwritten the necessary historical data. This is a classic architectural trade-off: you gain space efficiency but lose the ability to reconstruct the full solution path from the DP table alone.
Conclusion
Congratulations! You have now learned how to solve one of the most fundamental dynamic programming problems. This skill is not just academic; it's the foundation for solving real-world optimization problems in areas like resource allocation, financial modeling, and logistics.
Key Takeaways:
- The 0/1 Knapsack problem is about maximizing value within a capacity constraint.
- The core recurrence relation involves choosing whether to include or exclude the current item.
- Tabulation (bottom-up) builds a table of solutions iteratively, which is often very efficient.
- Memoization (top-down) uses recursion with a cache, which can be more intuitive to write from a recursive definition.
- You can backtrack through the DP table to find the specific items that constitute the optimal solution.
- The space complexity can be optimized from to , demonstrating a key engineering trade-off.
Preview of the Next Lesson:
In the next lesson, we will tackle another classic 2D dynamic programming problem: the Longest Common Subsequence (LCS). You'll see how the patterns and techniques you learned today—defining subproblems, formulating a recurrence, and building a DP table—can be adapted to solve a completely different type of problem.