Dynamic Programming: Knapsack Problem
Dynamic Programming is an algorithmic technique to solve constrained, combinatorial optimization problems. The Knapsack here is a metaphor for the constraint. The ‘knapsack’ might as well be a container ship.
Given a set of weights with corresponding values and a knapsack of certain capacity, the problem involves packing the most valuable knapsack with the weights that fit into the knapsack.
What is special about Dynamic Programming problems?
Dynamic programming problems can be broken down into subproblems that have optimal substructure. This means solving a subproblem optimally contributes to the optimal solution of the bigger problem. Additionally, the solutions to the subproblems are themselves overlapping or common to more than one containing problem.
The two approaches
The classical approach to dynamic programming is the bottom-up approach where we solve the major problem by solving each of the subordinate problems with a memory function.
The second approach is the top-down approach which starts with the major problem and recursively solves the subproblems whereby the solution bubbles up.
The first approach, because of the memory function, avoids solving the same subproblems repeatedly. However, it may end up solving some subproblems that do not contribute to the solution. The second approach solves only the necessary subproblems, but might end up solving them repeatedly.
We can indeed have the best of both worlds by combining top-down with memory function.
Recurrence equation and notations
We have weights \(w_i\) and corresponding values \(v_i\), \(i\) varying from \(1\) to \(n\). \(V_{i,j}\) is the value of the knapsack when weights upto \(i\) are available and the knapsack capacity is \(j\). We are seeking the solution \(V_{n,C}\) where \(C\) is the capacity of the knapsack.
At the heart of the algorithm are the two recurrence equations:
$$ V_{i,j} = V_{i-1,j},\ if\ j < w_i \tag{1} $$
$$ V_{i,j} = {max(V_{i-1,j}, v_i + V_{i-1, j-w_i})},\ if\ j >= w_i \tag{2} $$
$$ V_{0,j}, V_{i,0} = 0 \ for\ all\ i, j \tag{3} $$
Equation 1 means if the current available weight \(w_i\) is greater than the current available capacity \(j\) of the knapsack, then continue with the previous state.
Equation 2 means if the current available weight \(w_i\) is less than the current available capacity \(j\) of the knapsack, then determine whether adding \(w_i\) improves the value of the knapsack, over the previous state.
Equation 3 is meant for the initial condition: the first row and first column of solution matrix are 0s.
Implementation notes
We have implemented a class which supports both top-down and bottom-up approaches of the knapsack. Below, are the Python programs that we have implemented:
|
|
We get the following output:
Capacity: 10
Weights: [2, 1, 3, 2, 5, 7, 8] Values: [12, 10, 20, 15, 17, 28, 24]
Top down solution matrix:
[[ 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 12 12 12 12 12 12 12 12 12]
[ 0 10 12 22 -1 22 -1 22 22 -1 22]
[ 0 10 12 22 -1 32 -1 -1 42 -1 42]
[ 0 -1 15 25 -1 37 -1 -1 -1 -1 57]
[ 0 -1 15 25 -1 -1 -1 -1 -1 -1 57]
[ 0 -1 15 -1 -1 -1 -1 -1 -1 -1 57]
[ 0 -1 -1 -1 -1 -1 -1 -1 -1 -1 57]]
Best value: 57
Best weights: [1, 2, 3, 4]
Bottom up solution matrix:
[[ 0 0 0 0 0 0 0 0 0 0 0]
[ 0 0 12 12 12 12 12 12 12 12 12]
[ 0 10 12 22 22 22 22 22 22 22 22]
[ 0 10 12 22 30 32 42 42 42 42 42]
[ 0 10 15 25 30 37 45 47 57 57 57]
[ 0 10 15 25 30 37 45 47 57 57 57]
[ 0 10 15 25 30 37 45 47 57 57 57]
[ 0 10 15 25 30 37 45 47 57 57 57]]
Best value: 57
Best weights: [1, 2, 3, 4]
We observe that the bottom-up approach has solved all the sub-problems whereas the top-down approach has solved only the necessary sub-problems.
To get the latest version of the code, refer to this github link.