Introduction #

Given the weights and profits of ‘N’ items, we are asked to put these items in a knapsack which has a capacity ‘C’. The goal is to get the maximum profit out of the items in the knapsack. Each item can only be selected once, as we don’t have multiple quantities of any item.

Let’s take the example of Merry, who wants to carry some fruits in the knapsack to get maximum profit. Here are the weights and profits of the fruits:

Items: { Apple, Orange, Banana, Melon }
Weights: { 2, 3, 1, 4 }
Profits: { 4, 5, 3, 7 }
Knapsack capacity: 5

Let’s try to put various combinations of fruits in the knapsack, such that their total weight is not more than 5:

Apple + Orange (total weight 5) => 9 profit
Apple + Banana (total weight 3) => 7 profit
Orange + Banana (total weight 4) => 8 profit
Banana + Melon (total weight 5) => 10 profit

This shows that Banana + Melon is the best combination as it gives us the maximum profit and the total weight does not exceed the capacity.

Problem Statement #

Given two integer arrays to represent weights and profits of ‘N’ items, we need to find a subset of these items which will give us maximum profit such that their cumulative weight is not more than a given number ‘C’. Each item can only be selected once, which means either we put an item in the knapsack or we skip it.

Basic Solution #

A basic brute-force solution could be to try all combinations of the given items (as we did above), allowing us to choose the one with maximum profit and a weight that doesn’t exceed ‘C’. Take the example of four items (A, B, C, and D), as shown in the diagram below. To try all the combinations, our algorithm will look like:

--NORMAL--

Here is a visual representation of our algorithm:

widget

All green boxes have a total weight that is less than or equal to the capacity (7), and all the red ones have a weight that is more than 7. The best solution we have is with items [B, D] having a total profit of 22 and a total weight of 7.

Code #

Here is the code for the brute-force solution:

--NORMAL--

Output

3.774s

Total knapsack profit ---> 22 Total knapsack profit ---> 17

Time and Space complexity #

The time complexity of the above algorithm is exponential O(2n)O(2^n), where ‘n’ represents the total number of items. This can also be confirmed from the above recursion tree. As we can see, we will have a total of ‘31’ recursive calls – calculated through (2n)+(2n)1(2^n) + (2^n) - 1, which is asymptotically equivalent to O(2n)O(2^n).

The space complexity is O(n)O(n). This space will be used to store the recursion stack. Since the recursive algorithm works in a depth-first fashion, which means that we can’t have more than ‘n’ recursive calls on the call stack at any time.

Overlapping Sub-problems: Let’s visually draw the recursive calls to see if there are any overlapping sub-problems. As we can see, in each recursive call, profits and weights arrays remain constant, and only capacity and currentIndex change. For simplicity, let’s denote capacity with ‘c’ and currentIndex with ‘i’:

%0 node_1 c:7, i:0 node_2 c:6, i:1 node_1->node_2 node_4 c:7, i:1 node_1->node_4 node_1543793612308 c:4, i:2 node_2->node_1543793612308 node_1543793682700 c:6, i:2 node_2->node_1543793682700 node_1543793717174 c:5, i:2 node_4->node_1543793717174 node_1543793742749 c:7, i:2 node_4->node_1543793742749 node_1543793771246 c:1, i:3 node_1543793612308->node_1543793771246 node_1543793809791 c:4, i:3 node_1543793612308->node_1543793809791 node_1543793829375 c:3, i:3 node_1543793682700->node_1543793829375 node_1543794504038 c:6, i:3 node_1543793682700->node_1543794504038 node_1543793898118 c:2, i:3 node_1543793717174->node_1543793898118 node_1543793907062 c:5, i:3 node_1543793717174->node_1543793907062 node_1543793942638 c:4, i:3 node_1543793742749->node_1543793942638 node_1543793961745 c:7, i:3 node_1543793742749->node_1543793961745 node_1543796788726 c:1, i:4 node_1543794504038->node_1543796788726 node_1543796807255 c:6 i:4 node_1543794504038->node_1543796807255 node_1543796819102 c:0, i:4 node_1543793907062->node_1543796819102 node_1543796836302 c:5, i:4 node_1543793907062->node_1543796836302 node_1543796847374 c:2, i:4 node_1543793961745->node_1543796847374 node_1543796875431 c:7, i:4 node_1543793961745->node_1543796875431

We can clearly see that ‘c:4, i=3’ has been called twice. Hence we have an overlapping sub-problems pattern. We can use Memoization to efficiently solve overlapping sub-problems.

Top-down Dynamic Programming with Memoization #

Memoization is when we store the results of all the previously solved sub-problems and return the results from memory if we encounter a problem that has already been solved.

Since we have two changing values (capacity and currentIndex) in our recursive function knapsackRecursive(), we can use a two-dimensional array to store the results of all the solved sub-problems. As mentioned above, we need to store results for every sub-array (i.e. for every possible index ‘i’) and for every possible capacity ‘c’.

Code #

Here is the code with memoization (see changes in the highlighted lines):

--NORMAL--

Output

1.820s

Total knapsack profit ---> 22 Total knapsack profit ---> 17

Time and Space complexity #

Since our memoization array dp[profits.length][capacity+1] stores the results for all subproblems, we can conclude that we will not have more than NCN*C subproblems (where ‘N’ is the number of items and ‘C’ is the knapsack capacity). This means that our time complexity will be O(NC)O(N*C).

The above algorithm will use O(NC)O(N*C) space for the memoization array. Other than that we will use O(N)O(N) space for the recursion call-stack. So the total space complexity will be O(NC+N)O(N*C + N), which is asymptotically equivalent to O(NC).O(N*C).

Bottom-up Dynamic Programming #

Let’s try to populate our dp[][] array from the above solution by working in a bottom-up fashion. Essentially, we want to find the maximum profit for every sub-array and for every possible capacity. This means that dp[i][c] will represent the maximum knapsack profit for capacity ‘c’ calculated from the first ‘i’ items.

So, for each item at index ‘i’ (0 <= i < items.length) and capacity ‘c’ (0 <= c <= capacity), we have two options:

  1. Exclude the item at index ‘i’. In this case, we will take whatever profit we get from the sub-array excluding this item => dp[i-1][c]
  2. Include the item at index ‘i’ if its weight is not more than the capacity. In this case, we include its profit plus whatever profit we get from the remaining capacity and from remaining items => profit[i] + dp[i-1][c-weight[i]]

Finally, our optimal solution will be maximum of the above two values:

    dp[i][c] = max (dp[i-1][c], profit[i] + dp[i-1][c-weight[i]]) 

Let’s draw this visually and start with our base case of zero capacity:

Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 2 3 4 5 6 7
With '0' capacity, maximum profit we can have for every subarray is '0'
1 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 2 1 3 1 4 1 5 1 6 1 7 1
Capacity = 1-7, Index = 0, i.e., if we consider the sub-array till index '0', this means we have only one item to put in the knapsack, we will take it if it is not more than the capacity
2 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 2 1 3 1 4 1 5 1 6 1 7 1
Capacity = 1, Index =1, since item at index '1' has weight '2', which is greater than the capacity '1', so we will take the dp[index-1][capacity]
3 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 2 1 6 3 1 4 1 5 1 6 1 7 1
Capacity = 2, Index =1, from the formula discussed above: max( dp[0][2], profit[1] + dp[0][0] )
4 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 2 1 6 3 1 7 4 1 5 1 6 1 7 1
Capacity = 3, Index =1, from the formula discussed above: max( dp[0][3], profit[1] + dp[0][1] )
5 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 2 1 6 3 1 7 4 1 7 5 1 6 1 7 1
Capacity = 4, Index =1, from the formula discussed above: max( dp[0][4], profit[1] + dp[0][2] )
6 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 2 1 6 3 1 7 4 1 7 5 1 7 6 1 7 1
Capacity = 5, Index =1, from the formula discussed above: max( dp[0][5], profit[1] + dp[0][3] )
7 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 2 1 6 3 1 7 4 1 7 5 1 7 6 1 7 7 1
Capacity = 6, Index =1, from the formula discussed above: max( dp[0][6], profit[1] + dp[0][4] )
8 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 2 1 6 3 1 7 4 1 7 5 1 7 6 1 7 7 1 7
Capacity = 7, Index =1, from the formula discussed above: max( dp[0][7], profit[1] + dp[0][5] )
9 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 1 2 1 6 3 1 7 4 1 7 5 1 7 6 1 7 7 1 7
Capacity = 1, Index =2, since item at index '2' has weight '3', which is greater than the capacity '1', so we will take the dp[index-1][capacity]
10 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 1 2 1 6 6 3 1 7 4 1 7 5 1 7 6 1 7 7 1 7
Capacity = 2, Index =2, since item at index '2' has weight '3', which is greater than the capacity '1', so we will take the dp[index-1][capacity]
11 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 1 2 1 6 6 3 1 7 10 4 1 7 5 1 7 6 1 7 7 1 7
Capacity = 3, Index =2, from the formula discussed above: max( dp[1][3], profit[2] + dp[1][0] )
12 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 1 2 1 6 6 3 1 7 10 4 1 7 11 5 1 7 6 1 7 7 1 7
Capacity = 4, Index =2, from the formula discussed above: max( dp[1][4], profit[2] + dp[1][1] )
13 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 1 2 1 6 6 3 1 7 10 4 1 7 11 5 1 7 16 6 1 7 7 1 7
Capacity = 5, Index =2, from the formula discussed above: max( dp[1][5], profit[2] + dp[1][2] )
14 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 1 2 1 6 6 3 1 7 10 4 1 7 11 5 1 7 16 6 1 7 17 7 1 7
Capacity = 6, Index =2, from the formula discussed above: max( dp[1][6], profit[2] + dp[1][3] )
15 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 1 2 1 6 6 3 1 7 10 4 1 7 11 5 1 7 16 6 1 7 17 7 1 7 17
Capacity = 7, Index =2, from the formula discussed above: max( dp[1][7], profit[2] + dp[1][4] )
16 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 1 1 2 1 6 6 3 1 7 10 4 1 7 11 5 1 7 16 6 1 7 17 7 1 7 17
Capacity = 1, Index =3, since item at index '3' has weight '5', which is greater than the capacity '1', so we will take the dp[index-1][capacity]
17 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 1 1 2 1 6 6 6 3 1 7 10 4 1 7 11 5 1 7 16 6 1 7 17 7 1 7 17
Capacity = 2, Index =3, since item at index '3' has weight '5', which is greater than the capacity '2', so we will take the dp[index-1][capacity]
18 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 1 1 2 1 6 6 6 3 1 7 10 10 4 1 7 11 5 1 7 16 6 1 7 17 7 1 7 17
Capacity = 3, Index =3, since item at index '3' has weight '5', which is greater than the capacity '3', so we will take the dp[index-1][capacity]
19 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 1 1 2 1 6 6 6 3 1 7 10 10 4 1 7 11 11 5 1 7 16 6 1 7 17 7 1 7 17
Capacity = 4, Index =3, since item at index '3' has weight '5', which is greater than the capacity '4', so we will take the dp[index-1][capacity]
20 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 1 1 2 1 6 6 6 3 1 7 10 10 4 1 7 11 11 5 1 7 16 16 6 1 7 17 7 1 7 17
Capacity = 5, Index =3, from the formula discussed above: max( dp[2][5], profit[3] + dp[2][0] )
21 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 1 1 2 1 6 6 6 3 1 7 10 10 4 1 7 11 11 5 1 7 16 16 6 1 7 17 17 7 1 7 17
Capacity = 6, Index =3, from the formula discussed above: max( dp[2][6], profit[3] + dp[2][1] )
22 of 23
Created with Fabric.js 1.6.0-rc.1 capacity --> profit [] 1 6 10 16 weight [] 1 2 3 5 index 0 1 2 3 0 0 0 0 0 1 1 1 1 1 2 1 6 6 6 3 1 7 10 10 4 1 7 11 11 5 1 7 16 16 6 1 7 17 17 7 1 7 17 22
Capacity = 7, Index =3, from the formula discussed above: max( dp[2][7], profit[3] + dp[2][2] )
23 of 23

Code #

Here is the code for our bottom-up dynamic programming approach:

--NORMAL--

Output

2.825s

Total knapsack profit ---> 22 Total knapsack profit ---> 17

Time and Space complexity #

The above solution has the time and space complexity of O(NC)O(N*C), where ‘N’ represents total items and ‘C’ is the maximum capacity.

How can we find the selected items? #

As we know, the final profit is at the bottom-right corner. Therefore, we will start from there to find the items that will be going into the knapsack.

As you remember, at every step we had two options: include an item or skip it. If we skip an item, we take the profit from the remaining items (i.e. from the cell right above it); if we include the item, then we jump to the remaining profit to find more items.

Let’s understand this from the above example:

widget
  1. ‘22’ did not come from the top cell (which is 17); hence we must include the item at index ‘3’ (which is item ‘D’).
  2. Subtract the profit of item ‘D’ from ‘22’ to get the remaining profit ‘6’. We then jump to profit ‘6’ on the same row.
  3. ‘6’ came from the top cell, so we jump to row ‘2’.
  4. Again ‘6’ came from the top cell, so we jump to row ‘1’.
  5. ‘6’ is different than the top cell, so we must include this item (which is item ‘B’).
  6. Subtract the profit of ‘B’ from ‘6’ to get profit ‘0’. We then jump to profit ‘0’ on the same row. As soon as we hit zero remaining profit, we can finish our item search.
  7. Thus the items going into the knapsack are {B, D}.

Let’s write a function to print the set of items included in the knapsack.

--NORMAL--

Output

1.902s

Selected weights: 5 2 Total knapsack profit ---> 22 Selected weights: 3 2 1 Total knapsack profit ---> 17

Challenge #

Can we improve our bottom-up DP solution even further? Can you find an algorithm that has O(C)O(C) space complexity?

We only need one previous row to find the optimal solution!

--NORMAL--
Solution
0 of 2 Tests Passed
ResultInputExpected OutputActual OutputReason
solveKnapsack([1, 6, 10, 16], [1, 2, 3, 22-1Incorrect Output
solveKnapsack([1, 6, 10, 16], [1, 2, 3, 17-1Incorrect Output
3.272s

The solution above is similar to the previous solution, the only difference is that we use i%2 instead if i and (i-1)%2 instead if i-1. This solution has a space complexity of O(2C)=O(C)O(2*C) = O(C), where ‘C’ is the maximum capacity of the knapsack.

This space optimization solution can also be implemented using a single array. It is a bit tricky, but the intuition is to use the same array for the previous and the next iteration!

If you see closely, we need two values from the previous iteration: dp[c] and dp[c-weight[i]]

Since our inner loop is iterating over c:0-->capacity, let’s see how this might affect our two required values:

  1. When we access dp[c], it has not been overridden yet for the current iteration, so it should be fine.
  2. dp[c-weight[i]] might be overridden if “weight[i] > 0”. Therefore we can’t use this value for the current iteration.

To solve the second case, we can change our inner loop to process in the reverse direction: c:capacity-->0. This will ensure that whenever we change a value in dp[], we will not need it again in the current iteration.

Can you try writing this algorithm?

--NORMAL--
Solution
0 of 2 Tests Passed
ResultInputExpected OutputActual OutputReason
solveKnapsack([1, 6, 10, 16], [1, 2, 3, 22-1Incorrect Output
solveKnapsack([1, 6, 10, 16], [1, 2, 3, 17-1Incorrect Output
3.167s
Mark as Completed
←    Back
Introduction
Next    →
Equal Subset Sum Partition (medium)