Search improvements
Making backtracking faster: ordering the choices, branch and bound, symmetry breaking, constraint propagation and heuristic variable selection.
Learning goals
- Order choices so failures happen early
- Compute an optimistic bound and prune with it
- Eliminate symmetric branches
- Propagate forced moves before guessing
Prerequisites
Ordering choices
The order in which options are tried changes how much of the tree is visited, though not the answers. Two rules of thumb: try the **most constraining** choice first (a large item that fits few places, a vertex with many neighbours), and, for optimisation, try the **most promising** choice first so a good answer is found early and lets later bounds prune more.
Both pruned searches in this chapter rely on sorting: combination sum sorts candidates so it can break at the first overshoot, k-partition sorts values in descending order so groups overflow early, and the knapsack below sorts items by value per weight. Sorting costs O(n log n) once and can save exponential work.
0/1 knapsack: backtracking vs branch and bound
Interactive · edit the inputcurrent branch
solutions found (0)
items by value/weight, best first
Choose items to maximise value within capacity 10. Items are tried in order of value per weight. Before exploring a branch, an optimistic bound (fill the remaining room with fractions of the best items) is computed; a branch whose bound cannot beat the best known value is cut off.
1static int best;2static void search(int[] w, int[] v, int cap, int i, int weight, int value, boolean bound) { // items sorted by v/w, descending3if (i == w.length) { best = Math.max(best, value); return; }4if (bound && value + fractionalBound(w, v, cap - weight, i) <= best) return; // cannot beat the best: prune5if (weight + w[i] <= cap) // take item i, if it fits6search(w, v, cap, i + 1, weight + w[i], value + v[i], bound);7search(w, v, cap, i + 1, weight, value, bound); // skip item i8}
Live state
depth
0
solutions
0
Call stack (top first)
- empty
Execution trace (dry run table)Show 20 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | depth=0 solutions=0 | Choose items to maximise value within capacity 10. Items are tried in order of value per weight. Before exploring a branch, an optimistic bound (fill the remaining room with fractions of the best items) is computed; a branch whose bound cannot beat the best known value is cut off. |
| 2 | L2 | depth=1 solutions=0 | Explore weight 0/10, value 0, best 0. |
| 3 | L6 | depth=1 solutions=0 | Choose take #1. |
| 4 | L2 | depth=2 solutions=0 | Explore weight 3/10, value 50, best 0. |
| 5 | L6 | depth=2 solutions=0 | Choose take #2. |
| 6 | L2 | depth=3 solutions=0 | Explore weight 7/10, value 90, best 0. |
| 7 | L5 | depth=4 solutions=0 | Reject take #3: weight 13 exceeds the capacity 10. |
| 8 | L7 | depth=3 solutions=0 | Choose skip #3. |
| 9 | L2 | depth=4 solutions=0 | Explore weight 7/10, value 90, best 0. |
| 10 | L5 | depth=5 solutions=0 | Reject take #4: weight 12 exceeds the capacity 10. |
| 11 | L7 | depth=4 solutions=0 | Choose skip #4. |
| 12 | L2 | depth=5 solutions=0 | Explore weight 7/10, value 90, best 0. |
| 13 | L3 | depth=5 solutions=1 | Complete and valid: record value 90 (items [4, 2]). |
| 14 | L7 | depth=4 solutions=1 | Unchoose skip #4: back to weight 7/10, value 90, best 90. |
| 15 | L7 | depth=3 solutions=1 | Unchoose skip #3: back to weight 7/10, value 90, best 90. |
| 16 | L7 | depth=2 solutions=1 | Unchoose take #2: back to weight 3/10, value 50, best 90. |
| 17 | L4 | depth=3 solutions=1 | Reject skip #2: bound 82.0 cannot beat the best 90. |
| 18 | L7 | depth=1 solutions=1 | Unchoose take #1: back to weight 0/10, value 0, best 90. |
| 19 | L4 | depth=2 solutions=1 | Reject skip #1: bound 70.0 cannot beat the best 90. |
| 20 | L8 | solutions=1 nodes=9 return=90 | Search finished: 1 solution, 8 choices tried. Backtracking explores every branch the pruning rules allow. |
Branch and bound
For optimisation problems you do not need every solution, only the best. **Branch and bound** keeps the best value found so far and, at each node, computes an optimistic **bound** on the best value any completion could reach. If the bound cannot beat the best known value, the whole subtree is abandoned. For 0/1 knapsack, a valid bound is the fractional knapsack value of the remaining items: it relaxes the problem and is never smaller than the true answer.
With items 5:10, 4:40, 6:30, 3:50 and capacity 10, plain backtracking visits 29 nodes and finds the optimum 90; with the bound the same optimum is found after 9 nodes. The bound must be a true upper bound, otherwise a branch containing the optimum could be cut.
1static int best;23static int knapsackBb(int[] w, int[] v, int cap) { // items must be sorted by v/w, descending4best = 0;5search(w, v, cap, 0, 0, 0);6return best;7}8private static void search(int[] w, int[] v, int cap, int i, int weight, int value) {9if (i == w.length) { best = Math.max(best, value); return; }10if (value + fractionalBound(w, v, cap - weight, i) <= best) return; // cannot beat the best: prune11if (weight + w[i] <= cap) search(w, v, cap, i + 1, weight + w[i], value + v[i]); // take item i, if it fits12search(w, v, cap, i + 1, weight, value); // skip item i13}14private static double fractionalBound(int[] w, int[] v, int room, int from) {15double total = 0;16for (int k = from; k < w.length && room > 0; k++) {17int take = Math.min(room, w[k]);18total += (double) v[k] * take / w[k]; // the whole item, or the part that fits19room -= take;20}21return total;22}
0/1 knapsack: backtracking vs branch and bound
Interactive · edit the inputcurrent branch
solutions found (0)
items by value/weight, best first
Choose items to maximise value within capacity 10. Items are tried in order of value per weight. Before exploring a branch, an optimistic bound (fill the remaining room with fractions of the best items) is computed; a branch whose bound cannot beat the best known value is cut off.
1static int best;2static void search(int[] w, int[] v, int cap, int i, int weight, int value, boolean bound) { // items sorted by v/w, descending3if (i == w.length) { best = Math.max(best, value); return; }4if (bound && value + fractionalBound(w, v, cap - weight, i) <= best) return; // cannot beat the best: prune5if (weight + w[i] <= cap) // take item i, if it fits6search(w, v, cap, i + 1, weight + w[i], value + v[i], bound);7search(w, v, cap, i + 1, weight, value, bound); // skip item i8}
Live state
depth
0
solutions
0
Call stack (top first)
- empty
Execution trace (dry run table)Show 20 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | depth=0 solutions=0 | Choose items to maximise value within capacity 10. Items are tried in order of value per weight. Before exploring a branch, an optimistic bound (fill the remaining room with fractions of the best items) is computed; a branch whose bound cannot beat the best known value is cut off. |
| 2 | L2 | depth=1 solutions=0 | Explore weight 0/10, value 0, best 0. |
| 3 | L6 | depth=1 solutions=0 | Choose take #1. |
| 4 | L2 | depth=2 solutions=0 | Explore weight 3/10, value 50, best 0. |
| 5 | L6 | depth=2 solutions=0 | Choose take #2. |
| 6 | L2 | depth=3 solutions=0 | Explore weight 7/10, value 90, best 0. |
| 7 | L5 | depth=4 solutions=0 | Reject take #3: weight 13 exceeds the capacity 10. |
| 8 | L7 | depth=3 solutions=0 | Choose skip #3. |
| 9 | L2 | depth=4 solutions=0 | Explore weight 7/10, value 90, best 0. |
| 10 | L5 | depth=5 solutions=0 | Reject take #4: weight 12 exceeds the capacity 10. |
| 11 | L7 | depth=4 solutions=0 | Choose skip #4. |
| 12 | L2 | depth=5 solutions=0 | Explore weight 7/10, value 90, best 0. |
| 13 | L3 | depth=5 solutions=1 | Complete and valid: record value 90 (items [4, 2]). |
| 14 | L7 | depth=4 solutions=1 | Unchoose skip #4: back to weight 7/10, value 90, best 90. |
| 15 | L7 | depth=3 solutions=1 | Unchoose skip #3: back to weight 7/10, value 90, best 90. |
| 16 | L7 | depth=2 solutions=1 | Unchoose take #2: back to weight 3/10, value 50, best 90. |
| 17 | L4 | depth=3 solutions=1 | Reject skip #2: bound 82.0 cannot beat the best 90. |
| 18 | L7 | depth=1 solutions=1 | Unchoose take #1: back to weight 0/10, value 0, best 90. |
| 19 | L4 | depth=2 solutions=1 | Reject skip #1: bound 70.0 cannot beat the best 90. |
| 20 | L8 | solutions=1 nodes=9 return=90 | Search finished: 1 solution, 8 choices tried. Backtracking explores every branch the pruning rules allow. |
Symmetry breaking
Many search spaces contain **symmetric** branches that differ only by renaming: swapping two colours, two identical groups, the mirror image of a board. They cannot change whether a solution exists, so searching more than one of them is wasted work. Add a rule that keeps only one representative: for colouring, open **at most one new colour at a time** (colour c may be used only if colour c − 1 is already in use); for k-partition, try only the first of several identical **empty** groups.
K4 with 3 colours has no solution: the plain search visits 49 nodes, the symmetry-breaking search 12. For [4, 4, 4, 3, 3] in 3 groups of 6 the counts are 49 and 12 as well. On satisfiable inputs the saving is smaller, because the search stops at the first solution anyway.
1static boolean colorSym(int[][] adj, int v, int[] colors, int m) { // colors[] starts filled with -12if (v == adj.length) return true;3int maxUsed = -1; for (int c : colors) maxUsed = Math.max(maxUsed, c);4for (int c = 0; c < m; c++) {5if (c > maxUsed + 1) break; // colours are interchangeable: open at most one new colour6boolean clash = false;7for (int u : adj[v]) if (colors[u] == c) clash = true;8if (clash) continue;9colors[v] = c;10if (colorSym(adj, v + 1, colors, m)) return true;11colors[v] = -1;12}13return false;14}
Graph coloring with m colors
Interactive · edit the inputpartial solution
solutions found (0)
edges
Colour 4 vertices with 3 colours so that joined vertices differ. Vertices are coloured in order; a colour is rejected if a lower-numbered neighbour already uses it. With symmetry breaking only one new colour is opened at a time, because renaming colours never changes whether a colouring exists.
1static boolean color(int[][] adj, int v, int[] colors, int m, boolean symmetry) { // colors[] starts filled with -12if (v == adj.length) return true; // every vertex is coloured3int maxUsed = -1; for (int c : colors) maxUsed = Math.max(maxUsed, c);4for (int c = 0; c < m; c++) {5if (symmetry && c > maxUsed + 1) break; // colours are interchangeable: open at most one new colour6if (conflict(adj, v, c, colors)) continue; // a neighbour already has colour c7colors[v] = c; // choose8if (color(adj, v + 1, colors, m, symmetry)) return true; // explore9colors[v] = -1; // unchoose10}11return false;12}
Live state
depth
0
solutions
0
Call stack (top first)
- empty
Execution trace (dry run table)Show 15 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | depth=0 solutions=0 | Colour 4 vertices with 3 colours so that joined vertices differ. Vertices are coloured in order; a colour is rejected if a lower-numbered neighbour already uses it. With symmetry breaking only one new colour is opened at a time, because renaming colours never changes whether a colouring exists. |
| 2 | L1 | depth=1 solutions=0 | Explore colours [·, ·, ·, ·]. |
| 3 | L7 | depth=1 solutions=0 | Choose v0=c0. |
| 4 | L1 | depth=2 solutions=0 | Explore colours [c0, ·, ·, ·]. |
| 5 | L6 | depth=3 solutions=0 | Reject v1=c0: neighbour v0 already has colour c0. |
| 6 | L7 | depth=2 solutions=0 | Choose v1=c1. |
| 7 | L1 | depth=3 solutions=0 | Explore colours [c0, c1, ·, ·]. |
| 8 | L6 | depth=4 solutions=0 | Reject v2=c0: neighbour v0 already has colour c0. |
| 9 | L6 | depth=4 solutions=0 | Reject v2=c1: neighbour v1 already has colour c1. |
| 10 | L7 | depth=3 solutions=0 | Choose v2=c2. |
| 11 | L1 | depth=4 solutions=0 | Explore colours [c0, c1, c2, ·]. |
| 12 | L7 | depth=4 solutions=0 | Choose v3=c0. |
| 13 | L1 | depth=5 solutions=0 | Explore colours [c0, c1, c2, c0]. |
| 14 | L2 | depth=5 solutions=1 | Complete and valid: record [c0, c1, c2, c0]. |
| 15 | L12 | solutions=1 nodes=8 return=[c0, c1, c2, c0] | Search finished: 1 solution, 7 choices tried. Backtracking explores every branch the pruning rules allow. |
Constraint propagation
Instead of waiting for a dead end, **propagate** the consequences of each decision: when a choice removes candidates from other variables, check whether any variable is now forced (exactly one candidate left) or impossible (none left). Forced moves are filled in without guessing, and an impossible variable ends the branch immediately. In Sudoku the simplest form is the *naked single*: a cell with one remaining digit.
On the 4×4 puzzle with givens 1 at the corners the first-empty-cell strategy needs 12 placements, but with forced moves only 4 placements are guesses and 8 cells are filled by propagation. Pick “Most constrained + forced moves” in the animation and watch the “Forced by propagation” cells appear before each guess.
4×4 Sudoku: cell order and constraint propagation
Interactive · edit the inputgrid · 4 × 4
12 empty cells. Strategy: take the most constrained cell, and before every guess fill every cell that has a single candidate.
1static boolean solve(int[][] g, int mode) { // 0 = first empty cell, 1 = fewest candidates, 2 = fewest candidates + forced moves2List<int[]> forced = new ArrayList<>();3if (mode == 2 && !propagate(g, forced)) { undo(g, forced); return false; } // a cell with no candidate: dead end4int[] cell = mode == 0 ? firstEmpty(g) : mostConstrained(g);5if (cell == null) return true; // no empty cell: solved6for (int v = 1; v <= 4; v++) {7if (!legal(g, cell[0], cell[1], v)) continue; // row, column or box clash8g[cell[0]][cell[1]] = v; // choose9if (solve(g, mode)) return true; // explore10g[cell[0]][cell[1]] = 0; // unchoose11}12undo(g, forced);13return false;14}
Live state
placements
0
forced
0
Execution trace (dry run table)Show 19 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | placements=0 forced=0 | 12 empty cells. Strategy: take the most constrained cell, and before every guess fill every cell that has a single candidate. |
| 2 | L4 | placements=0 forced=0 | Next cell: row 1, column 2 with 3 candidates [2, 3, 4]. |
| 3 | L8 | placements=1 forced=0 | Guess 2 at row 1, column 2. |
| 4 | L4 | placements=1 forced=0 | Next cell: row 1, column 3 with 2 candidates [3, 4]. |
| 5 | L8 | placements=2 forced=0 | Guess 3 at row 1, column 3. |
| 6 | L3 | placements=2 forced=1 | Row 1, column 4 has only one candidate, 4: fill it without guessing. |
| 7 | L3 | placements=2 forced=2 | Row 2, column 4 has only one candidate, 2: fill it without guessing. |
| 8 | L3 | placements=2 forced=3 | Row 3, column 4 has only one candidate, 3: fill it without guessing. |
| 9 | L4 | placements=2 forced=3 | Next cell: row 2, column 1 with 2 candidates [3, 4]. |
| 10 | L8 | placements=3 forced=3 | Guess 3 at row 2, column 1. |
| 11 | L3 | placements=3 forced=4 | Row 2, column 2 has only one candidate, 4: fill it without guessing. |
| 12 | L3 | placements=3 forced=5 | Row 4, column 2 has only one candidate, 3: fill it without guessing. |
| 13 | L4 | placements=3 forced=5 | Next cell: row 3, column 1 with 2 candidates [2, 4]. |
| 14 | L8 | placements=4 forced=5 | Guess 2 at row 3, column 1. |
| 15 | L3 | placements=4 forced=6 | Row 3, column 3 has only one candidate, 4: fill it without guessing. |
| 16 | L3 | placements=4 forced=7 | Row 4, column 1 has only one candidate, 4: fill it without guessing. |
| 17 | L3 | placements=4 forced=8 | Row 4, column 3 has only one candidate, 2: fill it without guessing. |
| 18 | L5 | placements=4 forced=8 | No empty cell is left: the grid is solved. |
| 19 | L13 | placements=4 forced=8 return=1234/3412/2143/4321 | Solved with 4 placements, 0 dead ends and 8 forced fills. |
Heuristic variable selection
Which variable (cell, vertex, column) should the search branch on next? The **most-constrained-variable** (also called minimum remaining values) heuristic picks the one with the fewest legal options: it is the likeliest to fail, so failure is found high in the tree, and a variable with a single option is a free move. Algorithm X (next lesson) uses exactly this rule for columns.
On a 4×4 board the difference between “First empty cell” and “Most constrained first” is invisible: both place the same 12 digits for the default puzzle, because the board is tiny. The heuristic pays off on large instances such as 9×9 Sudoku or exact cover, where choosing the wrong variable can multiply the search by orders of magnitude. The point of the animation is the strategy switch; combine it with forced moves to see the effect.
1static int[] mostConstrained(int[][] g) { // 9x9 Sudoku: the empty cell with the fewest candidates2int[] best = null; int bestCount = 10;3for (int r = 0; r < 9; r++)4for (int c = 0; c < 9; c++) {5if (g[r][c] != 0) continue;6int count = 0;7for (int v = 1; v <= 9; v++) if (legalDigit(g, r, c, v)) count++;8if (count < bestCount) { bestCount = count; best = new int[]{r, c}; }9}10return best; // null when the grid is full11}12private static boolean legalDigit(int[][] g, int r, int c, int v) {13for (int k = 0; k < 9; k++) if (g[r][k] == v || g[k][c] == v) return false;14int br = r / 3 * 3, bc = c / 3 * 3;15for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) if (g[br + i][bc + j] == v) return false;16return true;17}
4×4 Sudoku: cell order and constraint propagation
Interactive · edit the inputgrid · 4 × 4
12 empty cells. Strategy: take the most constrained cell, and before every guess fill every cell that has a single candidate.
1static boolean solve(int[][] g, int mode) { // 0 = first empty cell, 1 = fewest candidates, 2 = fewest candidates + forced moves2List<int[]> forced = new ArrayList<>();3if (mode == 2 && !propagate(g, forced)) { undo(g, forced); return false; } // a cell with no candidate: dead end4int[] cell = mode == 0 ? firstEmpty(g) : mostConstrained(g);5if (cell == null) return true; // no empty cell: solved6for (int v = 1; v <= 4; v++) {7if (!legal(g, cell[0], cell[1], v)) continue; // row, column or box clash8g[cell[0]][cell[1]] = v; // choose9if (solve(g, mode)) return true; // explore10g[cell[0]][cell[1]] = 0; // unchoose11}12undo(g, forced);13return false;14}
Live state
placements
0
forced
0
Execution trace (dry run table)Show 19 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | placements=0 forced=0 | 12 empty cells. Strategy: take the most constrained cell, and before every guess fill every cell that has a single candidate. |
| 2 | L4 | placements=0 forced=0 | Next cell: row 1, column 2 with 3 candidates [2, 3, 4]. |
| 3 | L8 | placements=1 forced=0 | Guess 2 at row 1, column 2. |
| 4 | L4 | placements=1 forced=0 | Next cell: row 1, column 3 with 2 candidates [3, 4]. |
| 5 | L8 | placements=2 forced=0 | Guess 3 at row 1, column 3. |
| 6 | L3 | placements=2 forced=1 | Row 1, column 4 has only one candidate, 4: fill it without guessing. |
| 7 | L3 | placements=2 forced=2 | Row 2, column 4 has only one candidate, 2: fill it without guessing. |
| 8 | L3 | placements=2 forced=3 | Row 3, column 4 has only one candidate, 3: fill it without guessing. |
| 9 | L4 | placements=2 forced=3 | Next cell: row 2, column 1 with 2 candidates [3, 4]. |
| 10 | L8 | placements=3 forced=3 | Guess 3 at row 2, column 1. |
| 11 | L3 | placements=3 forced=4 | Row 2, column 2 has only one candidate, 4: fill it without guessing. |
| 12 | L3 | placements=3 forced=5 | Row 4, column 2 has only one candidate, 3: fill it without guessing. |
| 13 | L4 | placements=3 forced=5 | Next cell: row 3, column 1 with 2 candidates [2, 4]. |
| 14 | L8 | placements=4 forced=5 | Guess 2 at row 3, column 1. |
| 15 | L3 | placements=4 forced=6 | Row 3, column 3 has only one candidate, 4: fill it without guessing. |
| 16 | L3 | placements=4 forced=7 | Row 4, column 1 has only one candidate, 4: fill it without guessing. |
| 17 | L3 | placements=4 forced=8 | Row 4, column 3 has only one candidate, 2: fill it without guessing. |
| 18 | L5 | placements=4 forced=8 | No empty cell is left: the grid is solved. |
| 19 | L13 | placements=4 forced=8 return=1234/3412/2143/4321 | Solved with 4 placements, 0 dead ends and 8 forced fills. |
Practice ladder
Now try it yourself
Easy
- Why does sorting candidates help Combination Sum?
- Name one symmetry in N-Queens
Medium
- Partition to K Equal Sum Subsets (with symmetry breaking)
- 0/1 Knapsack with a bound
Advanced
- Sudoku Solver with naked singles
- Traveling Salesman by branch and bound (small n)
Key takeaways
- Order choices so failures come early; sort before searching.
- Branch and bound prunes with an optimistic bound and the best value found so far.
- Break symmetries, propagate forced moves, and branch on the most constrained variable.