Case Study 1: The Knapsack Problem Three Ways — Brute Force, Greedy, and Dynamic Programming
The Problem
A backpacker is packing for a hike. Their pack can hold at most 15 kg. They have 6 items to choose from:
| Item | Weight (kg) | Value (utility) | Value/Weight |
|---|---|---|---|
| Water filter | 2 | 15 | 7.50 |
| First aid kit | 3 | 12 | 4.00 |
| Sleeping bag | 5 | 20 | 4.00 |
| Tent | 7 | 25 | 3.57 |
| Extra food | 4 | 18 | 4.50 |
| Camera | 1 | 5 | 5.00 |
Which items should they take to maximize total utility?
Approach 1: Brute Force
Try all 2⁶ = 64 possible subsets and find the one with the highest value that fits within the weight limit.
procedure BruteForceKnapsack;
const
N = 6;
Cap = 15;
Weights: array[0..N-1] of Integer = (2, 3, 5, 7, 4, 1);
Values: array[0..N-1] of Integer = (15, 12, 20, 25, 18, 5);
Names: array[0..N-1] of string = (
'Water filter', 'First aid', 'Sleeping bag',
'Tent', 'Extra food', 'Camera');
var
Mask, BestMask: Integer;
I, TotalW, TotalV, BestV: Integer;
Subsets: Integer;
begin
BestV := 0;
BestMask := 0;
Subsets := 1 shl N; { 2^N }
for Mask := 0 to Subsets - 1 do begin
TotalW := 0;
TotalV := 0;
for I := 0 to N - 1 do
if (Mask and (1 shl I)) <> 0 then begin
TotalW := TotalW + Weights[I];
TotalV := TotalV + Values[I];
end;
if (TotalW <= Cap) and (TotalV > BestV) then begin
BestV := TotalV;
BestMask := Mask;
end;
end;
WriteLn('Brute Force Result (checked ', Subsets, ' subsets):');
WriteLn(' Total value: ', BestV);
Write(' Items: ');
for I := 0 to N - 1 do
if (BestMask and (1 shl I)) <> 0 then
Write(Names[I], ' (', Weights[I], 'kg) ');
WriteLn;
end;
Result: Water filter + First aid + Sleeping bag + Extra food + Camera = value 70, weight 15 kg. Subsets checked: 64. For N = 30: 2³⁰ ≈ 1 billion subsets. Impractical.
Approach 2: Greedy (by Value/Weight Ratio)
Sort items by value-per-weight ratio (descending). Take items greedily until the pack is full.
Sorted by ratio: Water filter (7.50), Camera (5.00), Extra food (4.50),
First aid (4.00), Sleeping bag (4.00), Tent (3.57)
Take Water filter: weight=2, value=15, remaining=13
Take Camera: weight=1, value=5, remaining=12
Take Extra food: weight=4, value=18, remaining=8
Take First aid: weight=3, value=12, remaining=5
Take Sleeping bag: weight=5, value=20, remaining=0
Cannot take Tent (weight=7 > remaining=0)
Greedy result: Value = 70, weight = 15 kg. Items: Water filter + Camera + Extra food + First aid + Sleeping bag.
In this case, greedy happens to find the optimal solution! But that is a coincidence, not a guarantee.
Approach 3: Dynamic Programming
Build a table where DP[i][w] = maximum value achievable using items 1..i with capacity w.
The DP table (abbreviated):
Cap: 0 1 2 3 4 5 ... 15
Item 0: 0 0 0 0 0 0 ... 0
Water(2,15): 0 0 15 15 15 15 ... 15
Aid(3,12): 0 0 15 15 15 27 ... 27
Bag(5,20): 0 0 15 15 15 27 ... 65
Tent(7,25): 0 0 15 15 15 27 ... 70
Food(4,18): 0 0 15 15 18 27 ... 70
Camera(1,5): 0 5 15 20 20 23 ... 70
DP result: Value = 70, weight = 15 kg. Same as brute force (guaranteed optimal).
When Greedy Fails
Modify the problem slightly: capacity = 10, items = [(6, 8), (5, 7), (5, 7)].
- Greedy (by ratio): Takes item 1 (ratio 1.33), weight = 6. Cannot fit item 2 or 3. Total value = 8.
- DP: Takes items 2 and 3, weight = 10. Total value = 14.
Greedy missed the optimal solution because it committed to the highest-ratio item, which used too much capacity.
Performance Comparison
| Approach | Time Complexity | Correctness | N = 6 | N = 30 | N = 100 |
|---|---|---|---|---|---|
| Brute Force | O(2ⁿ) | Always optimal | <1 ms | Hours | Impossible |
| Greedy | O(n log n) | Sometimes optimal | <1 ms | <1 ms | <1 ms |
| DP | O(n × W) | Always optimal | <1 ms | <1 ms | <1 ms (if W small) |
Key Lessons
- Brute force is correct but exponentially slow. Use only for tiny inputs or as a reference for testing.
- Greedy is fast and simple but not always correct. It works for the fractional knapsack but can fail for 0/1.
- DP is correct and efficient (pseudo-polynomial). It is the right choice for 0/1 knapsack in practice.
- Always test greedy against brute force on small inputs before trusting it on large ones.
Discussion Questions
- If you could take fractions of items, which approach would be optimal? (Hint: this changes the problem to fractional knapsack.)
- How would you modify the DP solution if you could take up to 3 copies of each item?
- The DP solution uses O(n × W) memory. How could you reduce this to O(W)?
- For very large W (e.g., W = 10⁹), DP becomes impractical. What approach would you use instead?