Chapter 20: Backtracking, Constraint Search and State Space
20.06Intermediate5 topics16 min5 interactive animations

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

Constraint problemsPartitioning and sum problems
20.06.1

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 input

Your input

Try

current branch

weight 0/10, value 0, best 0

solutions found (0)

none yet

items by value/weight, best first

3:50#4
4:40#2
6:30#3
5:10#1
Setup

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.

Step 1 / 20
Java
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, descending
3 if (i == w.length) { best = Math.max(best, value); return; }
4 if (bound && value + fractionalBound(w, v, cap - weight, i) <= best) return; // cannot beat the best: prune
5 if (weight + w[i] <= cap) // take item i, if it fits
6 search(w, v, cap, i + 1, weight + w[i], value + v[i], bound);
7 search(w, v, cap, i + 1, weight, value, bound); // skip item i
8}

Live state

depth

0

solutions

0

No condition evaluated on this step

Call stack (top first)

  1. empty
Execution trace (dry run table)Show 20 steps
StepLineStateWhat happens
1L2depth=0 solutions=0Choose 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.
2L2depth=1 solutions=0Explore weight 0/10, value 0, best 0.
3L6depth=1 solutions=0Choose take #1.
4L2depth=2 solutions=0Explore weight 3/10, value 50, best 0.
5L6depth=2 solutions=0Choose take #2.
6L2depth=3 solutions=0Explore weight 7/10, value 90, best 0.
7L5depth=4 solutions=0Reject take #3: weight 13 exceeds the capacity 10.
8L7depth=3 solutions=0Choose skip #3.
9L2depth=4 solutions=0Explore weight 7/10, value 90, best 0.
10L5depth=5 solutions=0Reject take #4: weight 12 exceeds the capacity 10.
11L7depth=4 solutions=0Choose skip #4.
12L2depth=5 solutions=0Explore weight 7/10, value 90, best 0.
13L3depth=5 solutions=1Complete and valid: record value 90 (items [4, 2]).
14L7depth=4 solutions=1Unchoose skip #4: back to weight 7/10, value 90, best 90.
15L7depth=3 solutions=1Unchoose skip #3: back to weight 7/10, value 90, best 90.
16L7depth=2 solutions=1Unchoose take #2: back to weight 3/10, value 50, best 90.
17L4depth=3 solutions=1Reject skip #2: bound 82.0 cannot beat the best 90.
18L7depth=1 solutions=1Unchoose take #1: back to weight 0/10, value 0, best 90.
19L4depth=2 solutions=1Reject skip #1: bound 70.0 cannot beat the best 90.
20L8solutions=1 nodes=9 return=90Search finished: 1 solution, 8 choices tried. Backtracking explores every branch the pruning rules allow.
20.06.2

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.

Java
1static int best;
2
3static int knapsackBb(int[] w, int[] v, int cap) { // items must be sorted by v/w, descending
4 best = 0;
5 search(w, v, cap, 0, 0, 0);
6 return best;
7}
8private static void search(int[] w, int[] v, int cap, int i, int weight, int value) {
9 if (i == w.length) { best = Math.max(best, value); return; }
10 if (value + fractionalBound(w, v, cap - weight, i) <= best) return; // cannot beat the best: prune
11 if (weight + w[i] <= cap) search(w, v, cap, i + 1, weight + w[i], value + v[i]); // take item i, if it fits
12 search(w, v, cap, i + 1, weight, value); // skip item i
13}
14private static double fractionalBound(int[] w, int[] v, int room, int from) {
15 double total = 0;
16 for (int k = from; k < w.length && room > 0; k++) {
17 int take = Math.min(room, w[k]);
18 total += (double) v[k] * take / w[k]; // the whole item, or the part that fits
19 room -= take;
20 }
21 return total;
22}

0/1 knapsack: backtracking vs branch and bound

Interactive · edit the input

Your input

Try

current branch

weight 0/10, value 0, best 0

solutions found (0)

none yet

items by value/weight, best first

3:50#4
4:40#2
6:30#3
5:10#1
Setup

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.

Step 1 / 20
Java
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, descending
3 if (i == w.length) { best = Math.max(best, value); return; }
4 if (bound && value + fractionalBound(w, v, cap - weight, i) <= best) return; // cannot beat the best: prune
5 if (weight + w[i] <= cap) // take item i, if it fits
6 search(w, v, cap, i + 1, weight + w[i], value + v[i], bound);
7 search(w, v, cap, i + 1, weight, value, bound); // skip item i
8}

Live state

depth

0

solutions

0

No condition evaluated on this step

Call stack (top first)

  1. empty
Execution trace (dry run table)Show 20 steps
StepLineStateWhat happens
1L2depth=0 solutions=0Choose 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.
2L2depth=1 solutions=0Explore weight 0/10, value 0, best 0.
3L6depth=1 solutions=0Choose take #1.
4L2depth=2 solutions=0Explore weight 3/10, value 50, best 0.
5L6depth=2 solutions=0Choose take #2.
6L2depth=3 solutions=0Explore weight 7/10, value 90, best 0.
7L5depth=4 solutions=0Reject take #3: weight 13 exceeds the capacity 10.
8L7depth=3 solutions=0Choose skip #3.
9L2depth=4 solutions=0Explore weight 7/10, value 90, best 0.
10L5depth=5 solutions=0Reject take #4: weight 12 exceeds the capacity 10.
11L7depth=4 solutions=0Choose skip #4.
12L2depth=5 solutions=0Explore weight 7/10, value 90, best 0.
13L3depth=5 solutions=1Complete and valid: record value 90 (items [4, 2]).
14L7depth=4 solutions=1Unchoose skip #4: back to weight 7/10, value 90, best 90.
15L7depth=3 solutions=1Unchoose skip #3: back to weight 7/10, value 90, best 90.
16L7depth=2 solutions=1Unchoose take #2: back to weight 3/10, value 50, best 90.
17L4depth=3 solutions=1Reject skip #2: bound 82.0 cannot beat the best 90.
18L7depth=1 solutions=1Unchoose take #1: back to weight 0/10, value 0, best 90.
19L4depth=2 solutions=1Reject skip #1: bound 70.0 cannot beat the best 90.
20L8solutions=1 nodes=9 return=90Search finished: 1 solution, 8 choices tried. Backtracking explores every branch the pruning rules allow.
20.06.3

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.

Java
1static boolean colorSym(int[][] adj, int v, int[] colors, int m) { // colors[] starts filled with -1
2 if (v == adj.length) return true;
3 int maxUsed = -1; for (int c : colors) maxUsed = Math.max(maxUsed, c);
4 for (int c = 0; c < m; c++) {
5 if (c > maxUsed + 1) break; // colours are interchangeable: open at most one new colour
6 boolean clash = false;
7 for (int u : adj[v]) if (colors[u] == c) clash = true;
8 if (clash) continue;
9 colors[v] = c;
10 if (colorSym(adj, v + 1, colors, m)) return true;
11 colors[v] = -1;
12 }
13 return false;
14}

Graph coloring with m colors

Interactive · edit the input

Your input

Try

partial solution

colours [·, ·, ·, ·]

solutions found (0)

none yet

edges

0-1
1-2
2-0
2-3
Setup

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.

Step 1 / 15
Java
1static boolean color(int[][] adj, int v, int[] colors, int m, boolean symmetry) { // colors[] starts filled with -1
2 if (v == adj.length) return true; // every vertex is coloured
3 int maxUsed = -1; for (int c : colors) maxUsed = Math.max(maxUsed, c);
4 for (int c = 0; c < m; c++) {
5 if (symmetry && c > maxUsed + 1) break; // colours are interchangeable: open at most one new colour
6 if (conflict(adj, v, c, colors)) continue; // a neighbour already has colour c
7 colors[v] = c; // choose
8 if (color(adj, v + 1, colors, m, symmetry)) return true; // explore
9 colors[v] = -1; // unchoose
10 }
11 return false;
12}

Live state

depth

0

solutions

0

No condition evaluated on this step

Call stack (top first)

  1. empty
Execution trace (dry run table)Show 15 steps
StepLineStateWhat happens
1L1depth=0 solutions=0Colour 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.
2L1depth=1 solutions=0Explore colours [·, ·, ·, ·].
3L7depth=1 solutions=0Choose v0=c0.
4L1depth=2 solutions=0Explore colours [c0, ·, ·, ·].
5L6depth=3 solutions=0Reject v1=c0: neighbour v0 already has colour c0.
6L7depth=2 solutions=0Choose v1=c1.
7L1depth=3 solutions=0Explore colours [c0, c1, ·, ·].
8L6depth=4 solutions=0Reject v2=c0: neighbour v0 already has colour c0.
9L6depth=4 solutions=0Reject v2=c1: neighbour v1 already has colour c1.
10L7depth=3 solutions=0Choose v2=c2.
11L1depth=4 solutions=0Explore colours [c0, c1, c2, ·].
12L7depth=4 solutions=0Choose v3=c0.
13L1depth=5 solutions=0Explore colours [c0, c1, c2, c0].
14L2depth=5 solutions=1Complete and valid: record [c0, c1, c2, c0].
15L12solutions=1 nodes=8 return=[c0, c1, c2, c0]Search finished: 1 solution, 7 choices tried. Backtracking explores every branch the pruning rules allow.
20.06.4

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 input

Your input

Try

grid · 4 × 4

0123
0
1
1
1
2
1
3
1
GivenGuessCell being filledForced by propagationDead end
Setup

12 empty cells. Strategy: take the most constrained cell, and before every guess fill every cell that has a single candidate.

Step 1 / 19
Java
1static boolean solve(int[][] g, int mode) { // 0 = first empty cell, 1 = fewest candidates, 2 = fewest candidates + forced moves
2 List<int[]> forced = new ArrayList<>();
3 if (mode == 2 && !propagate(g, forced)) { undo(g, forced); return false; } // a cell with no candidate: dead end
4 int[] cell = mode == 0 ? firstEmpty(g) : mostConstrained(g);
5 if (cell == null) return true; // no empty cell: solved
6 for (int v = 1; v <= 4; v++) {
7 if (!legal(g, cell[0], cell[1], v)) continue; // row, column or box clash
8 g[cell[0]][cell[1]] = v; // choose
9 if (solve(g, mode)) return true; // explore
10 g[cell[0]][cell[1]] = 0; // unchoose
11 }
12 undo(g, forced);
13 return false;
14}

Live state

0 placements0 forced fills0 dead ends

placements

0

forced

0

No condition evaluated on this step
Execution trace (dry run table)Show 19 steps
StepLineStateWhat happens
1L1placements=0 forced=012 empty cells. Strategy: take the most constrained cell, and before every guess fill every cell that has a single candidate.
2L4placements=0 forced=0Next cell: row 1, column 2 with 3 candidates [2, 3, 4].
3L8placements=1 forced=0Guess 2 at row 1, column 2.
4L4placements=1 forced=0Next cell: row 1, column 3 with 2 candidates [3, 4].
5L8placements=2 forced=0Guess 3 at row 1, column 3.
6L3placements=2 forced=1Row 1, column 4 has only one candidate, 4: fill it without guessing.
7L3placements=2 forced=2Row 2, column 4 has only one candidate, 2: fill it without guessing.
8L3placements=2 forced=3Row 3, column 4 has only one candidate, 3: fill it without guessing.
9L4placements=2 forced=3Next cell: row 2, column 1 with 2 candidates [3, 4].
10L8placements=3 forced=3Guess 3 at row 2, column 1.
11L3placements=3 forced=4Row 2, column 2 has only one candidate, 4: fill it without guessing.
12L3placements=3 forced=5Row 4, column 2 has only one candidate, 3: fill it without guessing.
13L4placements=3 forced=5Next cell: row 3, column 1 with 2 candidates [2, 4].
14L8placements=4 forced=5Guess 2 at row 3, column 1.
15L3placements=4 forced=6Row 3, column 3 has only one candidate, 4: fill it without guessing.
16L3placements=4 forced=7Row 4, column 1 has only one candidate, 4: fill it without guessing.
17L3placements=4 forced=8Row 4, column 3 has only one candidate, 2: fill it without guessing.
18L5placements=4 forced=8No empty cell is left: the grid is solved.
19L13placements=4 forced=8 return=1234/3412/2143/4321Solved with 4 placements, 0 dead ends and 8 forced fills.
20.06.5

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.

Java
1static int[] mostConstrained(int[][] g) { // 9x9 Sudoku: the empty cell with the fewest candidates
2 int[] best = null; int bestCount = 10;
3 for (int r = 0; r < 9; r++)
4 for (int c = 0; c < 9; c++) {
5 if (g[r][c] != 0) continue;
6 int count = 0;
7 for (int v = 1; v <= 9; v++) if (legalDigit(g, r, c, v)) count++;
8 if (count < bestCount) { bestCount = count; best = new int[]{r, c}; }
9 }
10 return best; // null when the grid is full
11}
12private static boolean legalDigit(int[][] g, int r, int c, int v) {
13 for (int k = 0; k < 9; k++) if (g[r][k] == v || g[k][c] == v) return false;
14 int br = r / 3 * 3, bc = c / 3 * 3;
15 for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) if (g[br + i][bc + j] == v) return false;
16 return true;
17}

4×4 Sudoku: cell order and constraint propagation

Interactive · edit the input

Your input

Try

grid · 4 × 4

0123
0
1
1
1
2
1
3
1
GivenGuessCell being filledForced by propagationDead end
Setup

12 empty cells. Strategy: take the most constrained cell, and before every guess fill every cell that has a single candidate.

Step 1 / 19
Java
1static boolean solve(int[][] g, int mode) { // 0 = first empty cell, 1 = fewest candidates, 2 = fewest candidates + forced moves
2 List<int[]> forced = new ArrayList<>();
3 if (mode == 2 && !propagate(g, forced)) { undo(g, forced); return false; } // a cell with no candidate: dead end
4 int[] cell = mode == 0 ? firstEmpty(g) : mostConstrained(g);
5 if (cell == null) return true; // no empty cell: solved
6 for (int v = 1; v <= 4; v++) {
7 if (!legal(g, cell[0], cell[1], v)) continue; // row, column or box clash
8 g[cell[0]][cell[1]] = v; // choose
9 if (solve(g, mode)) return true; // explore
10 g[cell[0]][cell[1]] = 0; // unchoose
11 }
12 undo(g, forced);
13 return false;
14}

Live state

0 placements0 forced fills0 dead ends

placements

0

forced

0

No condition evaluated on this step
Execution trace (dry run table)Show 19 steps
StepLineStateWhat happens
1L1placements=0 forced=012 empty cells. Strategy: take the most constrained cell, and before every guess fill every cell that has a single candidate.
2L4placements=0 forced=0Next cell: row 1, column 2 with 3 candidates [2, 3, 4].
3L8placements=1 forced=0Guess 2 at row 1, column 2.
4L4placements=1 forced=0Next cell: row 1, column 3 with 2 candidates [3, 4].
5L8placements=2 forced=0Guess 3 at row 1, column 3.
6L3placements=2 forced=1Row 1, column 4 has only one candidate, 4: fill it without guessing.
7L3placements=2 forced=2Row 2, column 4 has only one candidate, 2: fill it without guessing.
8L3placements=2 forced=3Row 3, column 4 has only one candidate, 3: fill it without guessing.
9L4placements=2 forced=3Next cell: row 2, column 1 with 2 candidates [3, 4].
10L8placements=3 forced=3Guess 3 at row 2, column 1.
11L3placements=3 forced=4Row 2, column 2 has only one candidate, 4: fill it without guessing.
12L3placements=3 forced=5Row 4, column 2 has only one candidate, 3: fill it without guessing.
13L4placements=3 forced=5Next cell: row 3, column 1 with 2 candidates [2, 4].
14L8placements=4 forced=5Guess 2 at row 3, column 1.
15L3placements=4 forced=6Row 3, column 3 has only one candidate, 4: fill it without guessing.
16L3placements=4 forced=7Row 4, column 1 has only one candidate, 4: fill it without guessing.
17L3placements=4 forced=8Row 4, column 3 has only one candidate, 2: fill it without guessing.
18L5placements=4 forced=8No empty cell is left: the grid is solved.
19L13placements=4 forced=8 return=1234/3412/2143/4321Solved 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.