Rec21 Knapsack
Rec21 Knapsack
K NAPSACK D P T OPSORT(n, S)
1 for i in {n, n − 1 . . . 0}
2 for j in {0, 1 . . . S}
3 print (i, j)
The full pseudo-code is straight-forward to write, as it closely follows the topological sort and
the Dynamic Programming recurrence.
K NAPSACK(n, S, s, v)
1 for i in {n, n − 1 . . . 0}
2 for j in {0, 1 . . . S}
3 if i == n
4 dp[i][j] = 0 // initial condition
5 else
6 choices = []
7 A PPEND(choices, dp[i + 1][j])
8 if j ≥ si
9 A PPEND(choices, dp[i + 1][j − si ] + vi )
10 dp[i][j] = M AX(choices)
11 return dp[0][S]
(2, 1)
0 -5
(3, 1) (3, 4)
Figure 1: Edges coming out of the vertex (2, 1) in a Knapsack problem instance where item 2
has weight 3 and value 5. If item 2 is selected, the new total weight will be 1 + 3 = 4, and the
total value will increase by 5. Edge weights are negative so that the shortest path will yield the
maximum knapsack value.
Running Time
The dynamic programming solution to the Knapsack problem requires solving O(nS) sub-problems.
The solution of one sub-problem depends on two other sub-problems, so it can be computed in
O(1) time. Therefore, the solution’s total running time is O(nS).
The DAG shortest-path solution creates a graph with O(nS) vertices, where each vertex has an
out-degree of O(1), so there are O(nS) edges. The DAG shortest-path algorithm runs in O(V +E),
so the solution’s total running time is also O(nS). This is reassuring, given that we’re performing
the same computation.
The solution running time is not polynomial in the input size. The next section explains the
subtle difference between polynomial running times and pseudo-polynomial running times, and
why it matters.
6.006 Introduction to Algorithms Recitation 19 November 23, 2011
• n item weights. We notice that item weights should be between 0 . . . S because we can
ignore any items whose weight exceeds the knapsack capacity. This means that each weight
can be represented using O(log S) bits, and all the weights will take up O(n log S) bits.
• n item values. Let V be the maximum value, so we can represent each value using O(log V )
bits, and all the values will take up O(n log V ) bits.
n s1 s2 ... sn v1 v2 ... vn
log n log S log S log S
Figure 2: A compact representation of an instance of the Knapsack problem. The item weights,
si , should be between 0 and S. An item whose weight exceeds the knapsack capacity (si > S) can
be immediately discarded from the input, as it wouldn’t fit the knapsack.
The total input size is O(log(n) + n(log S + log V )) = O(n(log S + log V )). Let b = log S,
v = log V , so the input size is O(n(v + b)). The running time for the Dynamic Programming
solution is O(nS) = O(n · 2b ). Does this make you uneasy? It should, and the next paragraph
explains why.
In this course, we use asymptotic analysis to understand the changes in an algorithm’s perfor-
mance as its input size increases – the corresponding buzzword is “scale”, as in “does algorithm X
scale?”. So, how does our Knapsack solution runtime change if we double the input size?
We can double the input size by doubling the number of items, so n0 = 2n. The running time
is O(nS), so we can expect that the running time will double. Linear scaling isn’t too bad!
However, we can also double the input size by doubling v and b, the number of bits required
to represent the item weights and values. v doesn’t show up in the running time, so let’s study
the impact of doubling the input size by doubling b. If we set b0 = 2b, the O(n · 2b ) result of our
algorithm analysis suggests that the running time will increase quadratically!
To drive this point home, Table 1 compares the solution’s running time for two worst-case
inputs of 6, 400 bits against a baseline worst-case input of 3, 200 bits.
6.006 Introduction to Algorithms Recitation 19 November 23, 2011
Table 1: The amounts of time required to solve some worst-case inputs to the Knapsack problem.
V E s1 t1 w1 s2 t2 w2 ... sE tE wE
log V log E log V log V log W log V log V log W log V log V log W
The total input size is O(log(V ) + log(E) + E(2 log V + log W )) = O(E(log V + log W )).
Let b = log W , so the input size is O(E(log V + b)).
1
A Fibonacci heaps-based implementation would have a running time of O(E + V log V ). Using that time would
our analysis a bit more difficult. Furthermore, Fibonacci heaps are only important to theorests, as their high constant
factors make them slow in practice.
6.006 Introduction to Algorithms Recitation 19 November 23, 2011
Note that the graph representation above, though intuitive, is not suitable for running Dijkstra.
However, the edge list representation can be used to build an adjacency list representation in O(V +
E), so the representation choice does not impact the total running time of Dijkstra’s algorithm,
which is O(E log V ).
Compare this result to the Knapsack solution’s running time. In this case, the running time is
polynomial (actually linear) in the number of bits required to represent the input. If we double the
input size by doubling E, the running time will double. If we double the input size by doubling the
width of the numbers used to represent V (in effect, we’d be squaring V , but not changing E), the
running time will also double. If we double the input size by doubling b, the width of the numbers
used to represent edge weights, the running time doesn’t change. No matter the reason why how
the input size doubles, there is no explosion in the running time.
Computation Model
Did that last paragraph make you uneasy? We just said that doubling the width of the numbers
used to represent the edge weights doesn’t change the running time at all. How can that be?!
We’re assuming the RAM computation model, where we can perform arithmetic operations on
word-sized numbers in O(1) time. This simplifies our analysis, but the main result above (Dijkstra
is polynomial, Knapsack’s solution is pseudo-polynomial) hold in any reasonable (deterministic)
computational model. You can convince yourself that, even if arithmetic operations take time
proportional to the inputs sizes (O(b), O(n), or O(log V ), depending on the case) Dijkstra’s run-
ning time is still polynomial in the input size, whereas the Dynamic Programming solution to the
Knapsack problem takes an amount of time that is exponential in b.
6.006 Introduction to Algorithms Recitation 19 November 23, 2011
Subset Sum
In a variant of the Knapsack problem, all the items in the vault are gold bars. The value of a gold
bar is directly proportional to its weight. Therefore, in order to make the most amount of money,
you must fill your knapsack up to its full capacity of S pounds. Can you find a subset of the gold
bars whose weights add up to exactly S?
We can define the familiar Boolean arithmetic operations A ND, O R, and N OT as follows. Let
a and b be Boolean variables.
1. Add bar i to the knapsack. In this case, we need to choose a subset of the bars i + 1 . . . n − 1
that weighs exactly j − si pounds. dp[i + 1][j − si ] indicates whether such a subset exists.
2. Don’t add item i to the knapsack. In this case, the solution rests on a subset of the bars
i + 1 . . . n − 1 that weighs exactly j pounds. The answer of whether such a subset exists is
in dp[i + 1][j].
6.006 Introduction to Algorithms Recitation 19 November 23, 2011
Either of the above avenues yields a solution, so dp[i][j] is T RUE if at least one of the decision
possibilities results in a solution. Recall that O R is implemented using max in our Boolean logic.
The recurrence (below) ends up being simpler than the one in the Knapsack solution!
dp[i + 1][j]
dp[i][j] = max
dp[i + 1][j − si ] if j ≥ si
The initial conditions for this problem are dp[n][0] = 1 (T RUE) and dp[n][j] = 0 (FALSE)
∀1 ≤ j ≤ S. The interval n . . . n − 1 contains no items, the corresponding knapsack is empty,
which means the only achievable weight is 0.
Just like in the Knapsack problem, the answer the original problem is in dp[0 ][S ]. The topo-
logical sort is identical to Knapsack’s, and the pseudo-code only has a few small differences.
S UBSET S UM(n, S, s)
1 for i in {n, n − 1 . . . 0}
2 for j in {0, 1 . . . S}
3 if i == n // initial conditions
4 if j == 0
5 dp[i][j] = 1
6 else
7 dp[i][j] = 0
8 else
9 choices = []
10 A PPEND(choices, dp[i + 1][j])
11 if j ≥ si
12 A PPEND(choices, dp[i + 1][j − si ])
13 dp[i][j] = M AX(choices)
14 return dp[0][S]
K-Sum
A further restriction of the Subset Sum problem is that the backpack has K pockets, and you can
fit exactly one gold bar in a pocket. You must fill up all the pockets, otherwise the gold bars will
move around as you walk, and the noises will trigger the vault’s alarm. The problem becomes:
given n gold bars of weights s0 . . . sn−1 , can you select exactly K bars whose weights add up to
exactly S?
1. Add bar i to the knapsack. In this case, we need to choose a k − 1-bar subset of the bars
i + 1 . . . n − 1 that weighs exactly j − si pounds. dp[i + 1][j − si ][k − 1] indicates whether
such a subset exists.
2. Don’t add item i to the knapsack. In this case, the solution rests on a k-bar subset of the bars
i + 1 . . . n − 1 that weighs exactly j pounds. The answer of whether such a subset exists is
in dp[i + 1][j][k].
Either of the above avenues yields a solution, so dp[i][j][k] is T RUE if at least one of the
decision possibilities results in a solution. The recurrence is below.
dp[i + 1][j][k]
dp[i][j][k] = max
dp[i + 1][j − si ][k − 1] if j ≥ si and k > 0
The initial conditions for this problem are dp[n][0][0] = 1 (T RUE) and dp[n][j][k] = 0 (FALSE)
∀1 ≤ j ≤ S, 0 ≤ k ≤ K. The interval n . . . n − 1 contains no items, the corresponding knapsack
is empty, which means the only achievable weight is 0.
The answer the original problem is in dp[0 ][S ][K ].
A topological sort can be produced by the following pseudo-code.
6.006 Introduction to Algorithms Recitation 19 November 23, 2011
D P T OPSORT(n, S )
1 for i in {n − 1, n − 2 . . . 0}
2 for j in {0, 1 . . . S}
3 for k in {0, 1 . . . K}
4 print (i, j, k)
Once again, the topological sort can be combined with the Dynamic Programming recurrence
to obtain the full solution pseudo-code in a straight-forward manner.
KS UM(n, K, S, s)
1 for i in {n, n − 1 . . . 0}
2 for j in {0, 1 . . . S}
3 for k in {0, 1 . . . K}
4 if i == n // initial conditions
5 if j == 0 and k == 0
6 dp[i][j][k] = 1
7 else
8 dp[i][j][k] = 0
9 else
10 choices = []
11 A PPEND(choices, dp[i + 1][j][k])
12 if j ≥ si and k > 0
13 A PPEND(choices, dp[i + 1][j − si ][k − 1])
14 dp[i][j][k] = M AX(choices)
15 return dp[0][S][K]
(2, 1)
0 1
(3, 1) (3, 4)
Figure 4: Edges coming out of the vertex (2, 1) in a k-Sum problem instance where gold bar 2 has
weight 3. If bar 2 is added to the knapsack, the new total weight will be 1 + 3 = 4, and the number
of occupied slots increases by 1.
maps to K edges from (i, j, k) to (i + 1, j, k) in the new graph. Each 1-cost edge from (i, j) to
(i, j + si ) maps to K edges (i, j, k) to (i, j + si , k + 1). A comparison between figures 5 and 4
illustrates the transformation.
(2, 1, 1)
(3, 1, 1)
(3, 4, 2)
Figure 5: Edges coming out of the vertex (2, 1, 1) in a k-Sum problem instance where gold bar 2
has weight 3. If bar 2 is added to the knapsack, the new total weight will be 1 + 3 = 4, and the
number of occupied slots increases by 1.
In the new graph, we want to find a path from vertex (0, 0, 0) to vertex (n, S, K). All edges
have the same weight.
Convince yourself that the DAG described above is equivalent to the dynamic programming
formulation in the previous section.
Running Time
The Dynamic Programming solution solves O(K · ns) sub-problems, and each sub-problem takes
O(1) time to solve. The solution’s total running time is O(KnS).
The DAG has K + 1 layers of O(nS) vertices (vertex count borrowed from the Knapsack
problem), and K copies of the O(nS) edges in the Knapsack graph. Therefore, V = O(K · S) and
E = O(Kc · S). The solution’s running time is O(V + E) = o(KnS)
6.006 Introduction to Algorithms Recitation 19 November 23, 2011