Skip to main content

Welcome to a Grasp lesson

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

Lesson 34

Dynamic Programming for 0/1 Knapsack

Recommended resources

Hello! Welcome to the first lesson in our module on classic Dynamic Programming applications.

In the previous module, we built a complete toolkit for DP: we learned to identify DP-suitable problems, formulate recursive solutions, convert them to memoized or tabulated forms, and finally, optimize them for space.

Today, we put that toolkit to work on one of the most famous problems in computer science: the 0/1 Knapsack problem. This lesson will guide you through solving this problem using a DP formulation, from understanding the core logic to implementing an efficient solution. It's a perfect example of how DP can systematically solve complex optimization problems where simpler approaches, like greedy algorithms, fall short.

1. Understanding the 0/1 Knapsack Problem

Imagine you're a burglar with a knapsack. You've entered a house with several items, each with a specific weight and value. Your knapsack has a maximum weight capacity. Your goal is to choose which items to steal to maximize the total value of your haul, without the total weight of the items exceeding your knapsack's capacity.

The "0/1" part of the name means that for each item, you have a binary choice: you either take the entire item (1) or you leave it behind (0). You cannot take a fraction of an item.

Let's formalize this:

  • You have items.
  • Each item has a weight and a value .
  • Your knapsack has a maximum weight capacity of .
  • You want to choose a subset of items that maximizes subject to .

Why Not a Greedy Approach?

Your first instinct might be to use a greedy strategy. For example:

  1. Take the most valuable items first.
  2. Take the lightest items first.
  3. Take the items with the highest value-to-weight ratio () first.

While these sound plausible, they don't guarantee an optimal solution. A single, high-value item might take up all the space, preventing you from picking several other items that, combined, would have been more valuable.

DP 19. 0/1 Knapsack | Recursion to Single Array Space Optimised Approach | DP on Subsequences

To see a concrete example of why a greedy approach fails, let's watch the beginning of this video from the 'take U forward' channel. It clearly illustrates a scenario where being greedy leads to a suboptimal outcome.

Watch from the start until 07:57. The video introduces the problem with the thief analogy and then provides a clear counterexample to show why a greedy strategy is not optimal for the 0/1 Knapsack problem.

Since greedy approaches fail, we need a method that explores all possibilities in a structured way to find the true optimum. This is where Dynamic Programming excels.


2. Formulating the DP Solution

As we've learned, the key to DP is to break the problem down into smaller, overlapping subproblems.

The Recursive Structure

Let's define our subproblem: "What is the maximum value we can obtain considering only the first i items with a knapsack capacity of w?"

Let's create a function, knapsack(i, w). When considering the -th item, we have two choices:

  1. Don't take the item: If we don't take item , the problem reduces to finding the maximum value we can get from the first items with the same capacity . The value is simply knapsack(i-1, w).
  2. Take the item: We can only do this if the item's weight, , is less than or equal to the remaining capacity, . If we take it, we gain its value, , but our remaining capacity decreases by . The problem then reduces to finding the maximum value from the first items with a capacity of . The value is v_i + knapsack(i-1, w - w_i).

Since we want to maximize the value, we take the best of these two choices. This gives us our recurrence relation:

The base cases for the recursion are:

  • If we have no items left () or no capacity left (), the maximum value is 0.

This recursive approach has exponential time complexity due to re-calculating the same subproblems repeatedly. We can optimize this using memoization (top-down) or tabulation (bottom-up). Let's focus on the tabulation method, as it's often more intuitive for visualizing the DP solution.

The Tabulation (Bottom-Up) Approach

We can solve this problem iteratively by building a 2D table (or matrix). Let dp[i][w] be the maximum value achievable using items from 1 to with a total capacity of .

  • The rows of our table will correspond to the items (from 0 to ).
  • The columns will correspond to the knapsack capacity (from 0 to ).

The final answer to our problem will be the value in the bottom-right cell, dp[n][W].

0/1 Knapsack Dynamic Programming Table
This image shows a typical DP table used for the Knapsack problem. The rows represent the items being considered, and the columns represent the increasing capacity of the knapsack. Each cell will store the optimal value for that subproblem.

We fill the table row by row, item by item. For each cell dp[i][w], we apply our recurrence relation:

dp[i][w] = max(dp[i-1][w], v[i-1] + dp[i-1][w - w[i-1]])

  • dp[i-1][w] corresponds to the "don't take item " case.
  • v[i-1] + dp[i-1][w - w[i-1]] corresponds to the "take item " case. (Note the i-1 index for v and w arrays if they are 0-indexed).

The 0/1 Knapsack Problem (Demystifying Dynamic Programming)

The best way to understand this is to see it in action. The following video from Back To Back SWE provides an exceptionally clear, step-by-step walkthrough of filling out the DP table for a sample Knapsack problem.

Watch from 04:37 to 16:48. Pay close attention to how each cell's value is determined by comparing two possibilities: taking the value from the cell directly above (don't include the current item) versus calculating a new value by including the current item and referencing a previous subproblem.


3. Reconstructing the Solution

The DP table gives us the maximum value, but what if we need to know which items were selected to achieve this value? We can find this by tracing our steps backward from the final cell dp[n][W].

Here's the logic:

  1. Start at dp[i][w] where i=n and w=W.
  2. Compare dp[i][w] with dp[i-1][w].
    • If they are equal (dp[i][w] == dp[i-1][w]), it means item was not included in the optimal solution for this subproblem. So, we move up to dp[i-1][w].
    • If they are not equal, it means item was included. We add item to our list of chosen items. Its inclusion contributed v[i-1] to the value, so we must have come from the subproblem before this item was added. We move diagonally up and to the left, to dp[i-1][w - w[i-1]].
  3. Repeat this process until we reach the first row ().

The 0/1 Knapsack Problem (Demystifying Dynamic Programming)

The same video from Back To Back SWE also demonstrates this backtracking process on the table we just filled.

Watch the segment from 16:48 to 18:39 to see how to reconstruct the set of items that form the optimal solution.


4. Complexity Analysis and Optimization

Let's analyze our tabulation solution.

  • Time Complexity: We are filling a 2D table of size . Each cell takes constant time to compute. Therefore, the time complexity is .
  • Space Complexity: We store the entire 2D table, so the space complexity is also .

This is known as a pseudo-polynomial time complexity. The runtime depends on the numeric value of the input W, not just the number of items n. If W is very large, this can be slow, but for typical constraints, it's very effective.

Space Optimization

This is a perfect moment to apply what we learned in the last lesson. Look at the recurrence relation again:
dp[i][w] = max(dp[i-1][w], ... + dp[i-1][...])

Notice that to compute the values for the current row i, we only ever need values from the previous row i-1. We never need to look at i-2, i-3, etc. This is the exact condition for applying the rolling array optimization!

We can reduce the space complexity from to by only keeping track of the previous row and the current row.

Even better, for the 0/1 Knapsack problem, we can optimize it down to a single 1D array of size . The trick is to iterate through the capacities w in reverse order (from W down to w_i). This ensures that when we compute dp[w], we are using the values from the previous item's iteration for dp[w - w_i], not the values we've just updated for the current item.

0/1 Knapsack Problem - GeeksforGeeks

The article '0/1 Knapsack Problem' on GeeksforGeeks provides a clear summary of all the approaches, including the final space-optimized one. The provided Python code is a great reference.

First, review the section '[Better Approach - 2] Using Bottom-Up DP (Tabulation)' to see the standard 2D array implementation. Then, read the section '[Expected Approach] Using Bottom-Up DP (Space-Optimized)' to see how it's optimized to a 1D array. Notice the inner loop iterates backward.

Here's the space-optimized Python code from the article for your reference:

def knapsack_optimized(W, val, wt):
    n = len(wt)






    # We only need a 1D array for DP
    dp = [0] * (W + 1)







    # Iterate through each item
    for i in range(n):






        # Iterate backwards through capacities
        for w in range(W, wt[i] - 1, -1):






            # Apply the recurrence
            dp[w] = max(dp[w], val[i] + dp[w - wt[i]])
    
    return dp[W]

Note that with this optimization, we lose the ability to easily backtrack and find the chosen items, as we no longer store the full table.


Conclusion

Congratulations on mastering your first classic DP problem! The 0/1 Knapsack is a cornerstone of algorithm studies because it beautifully demonstrates the power of DP to solve problems that are intractable with naive or greedy methods.

Key Takeaways:

  • The 0/1 Knapsack problem seeks to maximize value within a weight constraint.
  • Greedy strategies fail because local optimal choices do not guarantee a global optimum.
  • The DP solution relies on a recurrence relation with two choices for each item: take it or don't take it.
  • A bottom-up (tabulation) approach builds a 2D table dp[i][w] to find the optimal value. The time and space complexity are both .
  • The set of chosen items can be found by backtracking through the filled DP table.
  • The space complexity can be optimized from to by observing that each row only depends on the previous one.

Preview of the Next Lesson:
We will continue our journey through classic DP problems by tackling the Longest Common Subsequence (LCS) problem. This problem has a slightly different structure and is fundamental to many real-world applications, such as diff utilities (comparing file changes) and bioinformatics (analyzing DNA sequences).