CO4 Topic 31 of 34
Branch and bound: 0/1 knapsack
Branch and bound explores the same tree of choices as backtracking, but attaches an optimistic bound to every node and discards any node whose bound cannot beat the best answer found so far. Here it solves the 0/1 knapsack problem.
You will be able to
- Explain how branch and bound differs from backtracking
- Compute the fractional upper bound for a knapsack node
- Trace least-cost (best-first) branch and bound and identify pruned nodes
- Compare FIFO and least-cost search orders
Read
- Textbook 2, Ch. 8 Branch and bound (§8.1 the method, §8.2 0/1 knapsack)
- Textbook 5, Ch. on branch and bound
- Reference 1, §7.1 (pruning search)
Watch
Intuition
Backtracking prunes a node when it is infeasible. Branch and bound also prunes a node when it is not promising: for each node it computes a bound, the best value any solution below that node could possibly reach. If that bound is no better than the best complete solution already found, the whole subtree is skipped.
For the 0/1 knapsack (maximise value), a good optimistic bound is the fractional knapsack: fill the remaining capacity with the remaining items in value/weight order, and allow a fraction of the last item. No 0/1 solution can do better than that, because it obeys more rules.
Nodes are also explored in a smarter order. Instead of depth-first, branch and bound keeps a list of live nodes and picks the next one to expand:
- FIFO branch and bound: a queue, so the tree is explored level by level (breadth-first);
- least-cost (LC) branch and bound: a priority queue, always expanding the live node with the best bound first.
How it works
Sort the items by value/weight. At level k, decide on item k: the left child takes it (if it fits), the right child skips it.
bound(node) = value so far + the fractional-knapsack value of the remaining items in the remaining capacity
Example: W = 10, items (weight, value): 1 → (3, 30), 2 → (4, 32), 3 → (5, 35), 4 → (6, 30). The ratios 10, 8, 7 and 5 are already in decreasing order.
The root bound: take items 1 and 2 (weight 7, value 62), then 3/5 of item 3 → 62 + 21 = 83.
LC branch and bound, expanding the live node with the largest bound each time:
digraph G {
rankdir=TB; node [shape=box, style=rounded, fontsize=11];
edge [fontsize=10];
A [label="root\nv=0 w=0\nub=83"];
B [label="v=30 w=3\nub=83"]; C [label="v=0 w=0\nub=72"];
D [label="v=62 w=7\nub=83"]; E [label="v=30 w=3\nub=75"];
X1 [label="w=12 > 10\ninfeasible", color="#d64545"];
F [label="v=62 w=7\nub=77"];
G [label="v=65 w=8\nub=75"]; H [label="ub=60 ≤ 65\npruned", color="#d64545"];
I [label="v=32 w=4\nub=72"]; P2 [label="ub=60 ≤ 65\npruned", color="#d64545"];
J [label="v=67 w=9\nbest = 67", color="#2e9e5b", penwidth=3]; P3 [label="ub=62 ≤ 67\npruned", color="#d64545"];
A -> B [label="take 1"]; A -> C [label="skip 1"];
B -> D [label="take 2"]; B -> E [label="skip 2"];
D -> X1 [label="take 3"]; D -> F [label="skip 3"];
E -> G [label="take 3"]; E -> H [label="skip 3"];
C -> I [label="take 2"]; C -> P2 [label="skip 2"];
I -> J [label="take 3"]; I -> P3 [label="skip 3"];
}
| step | expand (bound) | what happens | best after |
|---|---|---|---|
| 1 | root (83) | take 1 → v = 30 (ub 83); skip 1 (ub 72) | 30 |
| 2 | take 1 (83) | take 2 → v = 62 (ub 83); skip 2 (ub 75) | 62 |
| 3 | take 1, 2 (83) | item 3 does not fit (w = 12); skip 3 (ub 77) | 62 |
| 4 | take 1, 2, skip 3 (77) | item 4 does not fit; skipping it gives only 62 | 62 |
| 5 | take 1, skip 2 (75) | take 3 → v = 65 (ub 75); skip 3 has ub 60 ≤ 65 → pruned | 65 |
| 6 | take 1, skip 2, take 3 (75) | item 4 does not fit | 65 |
| 7 | skip 1 (72) | take 2 → v = 32 (ub 72); skip 2 has ub 60 ≤ 65 → pruned | 65 |
| 8 | skip 1, take 2 (72) | take 3 → v = 67; skip 3 has ub 62 ≤ 67 → pruned | 67 |
| 9 | skip 1, take 2, take 3 (72) | item 4 does not fit | 67 |
No live node has a bound above 67, so the answer is 67: items 2 and 3 (weight 9). Greedy by ratio would stop at items 1 and 2 with value 62.
Python implementation
import heapq
def knapsack_bb(items, W):
"""items: list of (weight, value). Least-cost (best-first) branch and bound.
Return (best value, sorted indices of the chosen items)."""
order = sorted(range(len(items)), key=lambda i: items[i][1] / items[i][0], reverse=True)
def bound(k, w, v): # fractional knapsack on order[k:]
for i in order[k:]:
wt, val = items[i]
if w + wt <= W:
w, v = w + wt, v + val
else:
return v + (W - w) * val / wt
return v
best, best_set = 0, []
live = [(-bound(0, 0, 0), 0, 0, 0, [])] # max-heap on the bound
while live:
neg_ub, k, w, v, taken = heapq.heappop(live)
if -neg_ub <= best or k == len(order):
continue # cannot beat the best: prune
i = order[k]
wt, val = items[i]
if w + wt <= W: # child 1: take item i
if v + val > best:
best, best_set = v + val, taken + [i]
heapq.heappush(live, (-bound(k + 1, w + wt, v + val), k + 1, w + wt, v + val, taken + [i]))
if bound(k + 1, w, v) > best: # child 2: skip item i
heapq.heappush(live, (-bound(k + 1, w, v), k + 1, w, v, taken))
return best, sorted(best_set)
if __name__ == "__main__":
print(knapsack_bb([(3, 30), (4, 32), (5, 35), (6, 30)], 10)) # (67, [1, 2])
print(knapsack_bb([(10, 60), (20, 100), (30, 120)], 50)) # (220, [1, 2])
Replacing the heap with a collections.deque (append at the back, pop from the front) gives FIFO branch and bound.
Complexity
The tree has up to 2ⁿ⁺¹ − 1 nodes, so the worst case is still exponential. Each bound costs O(n), and each heap operation O(log of the number of live nodes). In practice a tight bound prunes most of the tree. Unlike the DP of topic 27, the running time does not depend on the size of W.
| Search order | Prunes on | |
|---|---|---|
| backtracking | depth-first | infeasibility |
| FIFO branch and bound | breadth-first (queue) | infeasibility and bound |
| LC branch and bound | best bound first (priority queue) | infeasibility and bound |
Common mistakes
- Forgetting to sort by value/weight before computing the fractional bound; otherwise the bound may be too low and prune the optimum.
- Pruning with "<" instead of "≤". Both give the right value, but "<" wastes time exploring nodes whose bound only ties the best.
- Not updating the best value at the take child. Every feasible partial selection is already a valid solution.
- Thinking branch and bound is polynomial. It is still exponential in the worst case.
Problems
- For W = 16 and items (2, 40), (5, 30), (10, 50), (5, 10), compute the root bound and draw the LC branch and bound tree.
- Lab: change
knapsack_bbto FIFO order and count the nodes expanded on the example. Which order expands fewer? - Lab: generate random instances with 25 items and compare the time of branch and bound with the DP of topic 27 as W grows.
- Textbook 2 phrases the knapsack as a minimisation problem with negative profits. Rewrite the bound in that form.
- Explain why the fractional knapsack value is always at least the 0/1 optimum.
Quiz
GATE-style practice
Questions in the GATE CSE formats: MCQ (one correct option), MSQ (one or more correct options) and NAT (type a number). Each comes with a full worked solution. These are original practice questions, not past papers; for the real thing, see the official GATE site and its previous question papers.