Reading
These notes.
0/1 Knapsack
The 0/1 Knapsack Problem is a classic problem in computer
science.
Problem: O/1 Knapsack
Given: capacity $C$ and $n$ items $(w_1,v_1), (w_2,v_2),
\ldots, (w_n,v_n)$ with "weight" and "value"
Find: a subset of items whose total weight does not exceed the
capacity $C$ and whose total value is maximized
It's called "0/1" because each item is either all in, or all
out. There is no possibility to put some portion of the item in
the knapsack.
randknap.cpp produces random
knapsack problems.
0/1 Knapsack: Greed is bad
A first attempt at an efficient 0/1 Knapsack algorithm is, of
course, a greedy approach. That means we need to pose the
problem as a series of "actions", or "choices" to make. For 0/1 Knapsack, this
is easy. We look at the last item in our list and decide for it
to either "put in knapsack", or "throw away". We then update by
removing it from the list of available objects and, if the
choice was "put in knapsack", reduce the capacity of the
knapsack by the object's weight.
We looked at several greedy choices and found that none worked,
i.e. none always produced optimal solutions.
0/1 Knapsack: a simple recursive example
So, we're going to have to try something other than a greedy
algorithm. As with the longest common subsequence problem,
we're going to start by considering an easier problem than the
true 0/1 knapsack. Instead of asking for which objects go into
the knapsack in order to achieve the maximum value, we will
simply try first to determine what the maximum achievable value
is. If we can solve that efficiently, maybe we can augment it
to solve the real 0/1 Knapsack problem. We'll call this problem
"0/1 Knapsack Value".
We get a simple recursive algorithm for 0/1 Knapsack Value
by considering the same actions available to our failed greedy
algorithms, but instead of picking one action and never going
back, we simply try both actions and see which gives the better
result. In other words, we proceed
by examining the last item from the list and recursively trying
out both adding it to the knapsack and not adding it to the
knapsack. Whichever gives a greater total value ... that's what
we go with.
int maxval(vector<Obj> &V, int n, int cap)
{
if (n == 0) return 0;
int v0 = maxval(V,n-1,cap);
int rem = cap - V[n-1].weight;
int v1 = rem < 0 ? -1 : V[n-1].value + maxval(V,n-1,rem);
return max(v0,v1);
}
knap.cpp
0/1 Knapsack: You've got to memoize
The above algorithm is calling out to be memoized. We discussed
how to do this in class.
knapmemo.cpp
Augmenting to recover the 0/1 Knapsack solution
Now we have a better algorithm for solving 0/1 Knapsack. Well
... a better algorithm for 0/1 Knapsack Value. We still haven't
solved the original 0/1 Knapsack problem. To do that, we need
to determin which weights to put in the knapsack in order to
achieve that maximum value. As with the Longest Common
Subsequence problem, the idea is to return to our greedy
algorithm. We can now run the original greedy algorithm, but
use the table from the memoized version to make the correct
greedy decision at each step. If the remaining capacity in the
knapsack is $C'$ and there are $n'$ objects left unconsidered,
here's what we do. If the rightmost unconsidered item is
$(w,v)$, we look at
$T[n'-1][C'- w]$ and $T[n'-1][C']$. Here's the rule:
If $T[n'-1][C'- w] + v \gt T[n'-1][C']$,
the item goes in the knapsack; otherwise, the item is
thrown away.
So do we have an efficient solution to 0/1 Knapsack?
Even with many items, our memoized solution is quite fast.
However, when the weights and capacity get large, this algorithm
is slow even with relatively few weights. In fact, we basically
have an $O(nC)$ algorithm, where $n$ is the number of items and
$C$ is the capacity. So is this "efficient", or not?
In the coming lessons, we'll have more to say about this!