Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 7

Dynamic Programming: 0/1 Knapsack Problem

Recommended resources

Welcome to our next lesson. In the previous session, we covered Breadth-First and Depth-First Search, the essential algorithms for traversing graphs. We saw how their different strategies—exploring level-by-level versus path-by-path—make them suitable for distinct types of problems, like finding the shortest path versus exploring all possibilities.

Today, we transition from traversal algorithms to a powerful problem-solving paradigm known as Dynamic Programming (DP). DP is a cornerstone of algorithm design, especially for optimization problems where you need to find the best possible solution among many. Your goal for this lesson is to learn how to solve the classic 0/1 Knapsack problem using dynamic programming. This problem is a perfect vehicle for understanding the core principles of DP, which are broadly applicable in areas like resource allocation, scheduling, and financial modeling.

The 0/1 Knapsack Problem: A Classic Dilemma

Imagine you are a burglar (for academic purposes, of course) with a knapsack that can hold a limited total weight. You find a stash of items, each with its own weight and value. Your task is to decide which items to steal to maximize the total value of your haul, without exceeding the knapsack's weight capacity.

This is the essence of the 0/1 Knapsack problem. The "0/1" part is crucial: for each item, you have only two choices—either take the whole item (1) or leave it behind (0). You cannot take a fraction of an item.

0/1 Knapsack Problem

The GeeksforGeeks article provides a concise definition of the problem.

Read the problem description and the two examples to solidify your understanding of the inputs and the desired output.


The Obvious (but Flawed) Approaches

Before diving into dynamic programming, let's consider more intuitive ways to solve this and see why they fall short.

1. Brute Force: Trying Every Combination

The most straightforward approach is to generate every possible subset of items. For each subset, you'd check if its total weight is within the knapsack's capacity. If it is, you calculate its total value. Finally, you pick the subset with the highest value.

This method guarantees the correct answer, but at a steep cost. For items, there are possible subsets. For each item, you can either take it or leave it, leading to a decision tree that grows exponentially.

This recursion tree illustrates the brute-force approach. At each node, we decide whether to select or skip an item, leading to two branches. The number of leaf nodes, representing all possible combinations, grows as \(2^N\).

This exponential time complexity, , makes the brute-force approach impractical for all but the smallest inputs.

2. The Greedy Mistake

Another idea is to use a greedy strategy. For example, you could calculate the value-to-weight ratio for each item and prioritize picking items with the highest ratio. While this sounds logical, it doesn't guarantee an optimal solution for the 0/1 version of the problem. A high-ratio item might take up weight that could be better used by a combination of other, slightly less "efficient" items.

The resource below gives a clear counter-example demonstrating why a greedy approach fails.

Solving the 0/1 Knapsack Problem with Dynamic Programming

This section from the Dynamic Programming Workbook explains why a simple greedy algorithm isn't sufficient.

In the "Thinking About the Problem" section, focus on the first bullet point that asks, "would a greedy algorithm work here?". The provided counter-example is a perfect illustration of its flaw.


The Dynamic Programming Solution: Remember Your Work

The brute-force recursive approach is slow because it repeatedly solves the same subproblems. For instance, the maximum value for a knapsack of capacity 10kg with the first 5 items might be calculated multiple times through different decision paths.

Dynamic Programming tackles this inefficiency by storing the results of subproblems. It's built on two key properties:

  1. Optimal Substructure: The optimal solution for a problem can be constructed from the optimal solutions of its subproblems.
  2. Overlapping Subproblems: The problem can be broken down into subproblems that are reused several times.

Let's explore the two main DP strategies: top-down with memoization and bottom-up with tabulation.

Top-Down DP with Memoization

This approach is a natural extension of the brute-force recursive solution. We write the same recursive function but add a "memo" or cache to store the results of function calls. The state of a subproblem is defined by two parameters: the index of the current item being considered (n) and the remaining capacity of the knapsack (capacity).

The logic is:

  1. Before computing knapsack(n, capacity), check if the result is already in our memo.
  2. If it is, return the stored value immediately.
  3. If not, compute the result recursively as before.
  4. Before returning, store the newly computed result in the memo.

This simple caching strategy transforms the exponential complexity into pseudo-polynomial time, , where is the number of items and is the capacity.

Solving the 0/1 Knapsack Problem with Dynamic Programming

This article provides an excellent narrative on evolving the recursive solution into a memoized one.

First, read Defining the Recursion Relation to understand the core logic that underpins all the solutions. Next, review A First Solution to see the pure recursive implementation. Finally, read A First Pass at Optimization, which introduces the memoization cache and shows the optimized code.

Bottom-Up DP with Tabulation

The bottom-up approach, also known as tabulation, is often what people visualize when they think of dynamic programming. Instead of starting from the top (the main problem) and recursing down, we start from the smallest subproblems and build our way up to the final solution.

We use a 2D table, let's call it dp, where dp[i][w] represents the maximum value we can obtain using the first i items with a knapsack capacity of w. The table dimensions will be (N+1) x (W+1).

For each cell dp[i][w], we make a decision about the i-th item:

  • Don't include item i: In this case, the maximum value is simply the best we could do with the first i-1 items and the same capacity w. This value is dp[i-1][w].
  • Include item i (only if weight[i] <= w): The value would be value[i] plus the maximum value we could get from the remaining capacity (w - weight[i]) using the first i-1 items. This value is value[i] + dp[i-1][w - weight[i]].

The value of dp[i][w] is the maximum of these two choices. The final answer to our problem will be in the bottom-right cell of the table, dp[N][W].

The following video provides an exceptionally clear and intuitive walkthrough of building this DP table from the bottom up.

The 0/1 Knapsack Problem (Demystifying Dynamic Programming)

This video from Back To Back SWE demystifies the bottom-up approach by carefully explaining the logic at each step.

Watch the segment from the bottom-up walkthrough. Pay close attention to how each cell is filled by making a choice: either take the value from the cell directly above (don't include the current item) or calculate a new potential value by including the current item. This visual explanation is key to building intuition.

After watching the walkthrough, you can reference the clean Python implementation of this tabulation approach in the resources from GeeksforGeeks or the DP Workbook.


A Critical Optimization: Reducing Space

The tabulation approach has a time complexity of and a space complexity of to store the DP table. For problems with a very large capacity W, this space usage can be prohibitive.

Notice that to compute the values for the current row i, we only need the values from the previous row i-1. This means we don't need to store the entire table. We can optimize the space to just two rows, .

We can do even better. A common DP trick allows us to reduce the space to a single 1D array of size , bringing the space complexity down to . The key is to iterate through the capacities in reverse order (from W down to the current item's weight). This ensures that when we calculate dp[w], the value of dp[w - weight[i]] we use is from the previous iteration (as if it were dp[i-1]), not one that has already been updated in the current iteration.

Solving the 0/1 Knapsack Problem with Dynamic Programming

This article clearly explains the logic behind space optimization for the bottom-up solution.

Read the sections Converting to Bottom-Up and Optimizing the Bottom-Up solution. The second section explains the clever trick of using a single array and iterating backwards to achieve O(W) space.


Reconstructing the Solution

Our DP table gives us the maximum value, but it doesn't explicitly tell us which items we chose to achieve it. To find the items, we can backtrack through the filled DP table, starting from the final cell dp[N][W].

  1. Start at dp[N][W].
  2. Compare dp[i][w] with dp[i-1][w].
    • If the values are different, it means item i was included in the optimal solution for this subproblem. Add item i to our solution set, subtract its weight from the current capacity (w = w - weight[i]), and move up to the previous row (i = i-1).
    • If the values are the same, it means item i was not included. Move up to the previous row (i = i-1) at the same capacity.
  3. Repeat this process until you reach the top of the table.

The Back To Back SWE video you watched earlier also includes a great, quick demonstration of this backtracking process.

The 0/1 Knapsack Problem (Demystifying Dynamic Programming)

This segment shows how to walk back through the table to find the items that constitute the optimal solution.

Watch the short section on reconstructing the solution. The logic of comparing the current cell with the one above it is the key takeaway.


Conclusion

In this lesson, we tackled the 0/1 Knapsack problem and used it as a lens to understand the power of dynamic programming. You've seen how to progress from an inefficient brute-force solution to a highly optimized one.

Here are the key takeaways:

  • 0/1 Knapsack is an optimization problem where you must choose items to maximize value within a weight constraint, with the rule that you must take an item fully or not at all.
  • Brute-force recursion is simple but has an exponential time complexity of .
  • Dynamic Programming is effective because the problem exhibits optimal substructure and overlapping subproblems.
  • Top-Down DP (Memoization) enhances the recursive solution by caching results, improving time complexity to .
  • Bottom-Up DP (Tabulation) builds a table of solutions to subproblems, also with time complexity. It is often more intuitive for implementation.
  • Space Optimization can reduce the bottom-up approach's space complexity from to just by using a single array and iterating backwards.

This lesson concludes our deep dive into specific algorithms. In our next and final lesson for this module, we will synthesize everything you've learned. The goal will be to develop a framework for analyzing a problem and strategically selecting the most appropriate data structure and algorithm to solve it, a critical skill for any engineering role.