Chapter 20: Backtracking, Constraint Search and State Space
20.03Intermediate6 topics22 min6 interactive animations

Constraint problems

Constraint search problems: N-Queens, Sudoku, word search, a rat in a maze, graph coloring and Hamiltonian paths, each as choose-explore-unchoose with a validity check.

Learning goals

  • Encode constraints as O(1) checks
  • Trace N-Queens and read the attacked squares
  • Run Sudoku with candidate lists
  • Search a grid with a visited marker that is restored
  • Explain why graph colouring and Hamiltonian path are hard in general

Prerequisites

The backtracking methodSubset and permutation generationMatrices and grids
20.03.1

N-Queens

Place n queens on an n×n board so that no two share a row, column or diagonal. Place one queen per row, top to bottom; the only decision at each row is the column. A square is unsafe if an earlier queen is in the same column or on either diagonal. Three boolean arrays answer that in O(1): usedCol[c], diag1[row − c + n − 1] (one diagonal direction) and diag2[row + c] (the other).

Run the presets: 4 queens finds [2, 4, 1, 3] after 8 placements and 4 backtracks; both solutions of N = 4 need 16 placements; the 10 solutions of N = 5 need 53; the first solution for N = 6, [2, 4, 6, 1, 3, 5], needs 31 placements and 25 backtracks. Dimmed squares are attacked by a queen above.

n45678910
Solutions21044092352724
Java
1static int solve(int n, int row, int[] cols, boolean[] usedCol, boolean[] diag1, boolean[] diag2) { // counts solutions
2 if (row == n) return 1;
3 int count = 0;
4 for (int c = 0; c < n; c++) {
5 if (usedCol[c] || diag1[row - c + n - 1] || diag2[row + c]) continue; // attacked by a queen above
6 usedCol[c] = diag1[row - c + n - 1] = diag2[row + c] = true; cols[row] = c; // choose
7 count += solve(n, row + 1, cols, usedCol, diag1, diag2); // explore
8 usedCol[c] = diag1[row - c + n - 1] = diag2[row + c] = false; // unchoose
9 }
10 return count;
11}
12static int queens(int n) { return solve(n, 0, new int[n], new boolean[n], new boolean[2 * n - 1], new boolean[2 * n - 1]); }

N-Queens by backtracking

Interactive · edit the input

Your input

Try

grid · 4 × 4

0123
0
1
2
3
Queen placedSquare being triedAttacked: rejectedSolutionAttacked by a queen above
Setup

Place one queen per row, top to bottom. A square is attacked when a queen above shares its column or a diagonal. If a row has no safe square, back up and move the previous queen. Stop at the first complete board.

Step 1 / 33
Java
1static int solve(int n, int row, int[] cols, boolean[] usedCol, boolean[] diag1, boolean[] diag2, boolean firstOnly) {
2 if (row == n) return 1; // all n queens placed
3 int count = 0;
4 for (int c = 0; c < n; c++) {
5 if (usedCol[c] || diag1[row - c + n - 1] || diag2[row + c]) continue; // attacked by a queen above
6 usedCol[c] = diag1[row - c + n - 1] = diag2[row + c] = true; cols[row] = c; // choose
7 count += solve(n, row + 1, cols, usedCol, diag1, diag2, firstOnly); // explore
8 if (firstOnly && count > 0) return count; // keep the board of the first solution
9 usedCol[c] = diag1[row - c + n - 1] = diag2[row + c] = false; // unchoose
10 }
11 return count;
12}

Live state

0 placements0 backtracks

row

0

queens

0

solutions

0

No condition evaluated on this step
Execution trace (dry run table)Show 33 steps
StepLineStateWhat happens
1L2row=0 queens=0 solutions=0Place one queen per row, top to bottom. A square is attacked when a queen above shares its column or a diagonal. If a row has no safe square, back up and move the previous queen. Stop at the first complete board.
2L6row=1 queens=1 solutions=0Row 1, column 1 is safe: place a queen.
3L5row=1 queens=1 solutions=0Row 2, column 1: attacked by the queen at row 1, column 1. Try the next column.
4L5row=1 queens=1 solutions=0Row 2, column 2: attacked by the queen at row 1, column 1. Try the next column.
5L6row=2 queens=2 solutions=0Row 2, column 3 is safe: place a queen.
6L5row=2 queens=2 solutions=0Row 3, column 1: attacked by the queen at row 1, column 1. Try the next column.
7L5row=2 queens=2 solutions=0Row 3, column 2: attacked by the queen at row 2, column 3. Try the next column.
8L5row=2 queens=2 solutions=0Row 3, column 3: attacked by the queen at row 1, column 1. Try the next column.
9L5row=2 queens=2 solutions=0Row 3, column 4: attacked by the queen at row 2, column 3. Try the next column.
10L9row=1 queens=1 solutions=0Remove the queen from row 2, column 3: back to row 2 to try the next column.
11L6row=2 queens=2 solutions=0Row 2, column 4 is safe: place a queen.
12L5row=2 queens=2 solutions=0Row 3, column 1: attacked by the queen at row 1, column 1. Try the next column.
13L6row=3 queens=3 solutions=0Row 3, column 2 is safe: place a queen.
14L5row=3 queens=3 solutions=0Row 4, column 1: attacked by the queen at row 1, column 1. Try the next column.
15L5row=3 queens=3 solutions=0Row 4, column 2: attacked by the queen at row 2, column 4. Try the next column.
16L5row=3 queens=3 solutions=0Row 4, column 3: attacked by the queen at row 3, column 2. Try the next column.
17L5row=3 queens=3 solutions=0Row 4, column 4: attacked by the queen at row 1, column 1. Try the next column.
18L9row=2 queens=2 solutions=0Remove the queen from row 3, column 2: back to row 3 to try the next column.
19L5row=2 queens=2 solutions=0Row 3, column 3: attacked by the queen at row 1, column 1. Try the next column.
20L5row=2 queens=2 solutions=0Row 3, column 4: attacked by the queen at row 2, column 4. Try the next column.
21L9row=1 queens=1 solutions=0Remove the queen from row 2, column 4: back to row 2 to try the next column.
22L9row=0 queens=0 solutions=0Remove the queen from row 1, column 1: back to row 1 to try the next column.
23L6row=1 queens=1 solutions=0Row 1, column 2 is safe: place a queen.
24L5row=1 queens=1 solutions=0Row 2, column 1: attacked by the queen at row 1, column 2. Try the next column.
25L5row=1 queens=1 solutions=0Row 2, column 2: attacked by the queen at row 1, column 2. Try the next column.
26L5row=1 queens=1 solutions=0Row 2, column 3: attacked by the queen at row 1, column 2. Try the next column.
27L6row=2 queens=2 solutions=0Row 2, column 4 is safe: place a queen.
28L6row=3 queens=3 solutions=0Row 3, column 1 is safe: place a queen.
29L5row=3 queens=3 solutions=0Row 4, column 1: attacked by the queen at row 3, column 1. Try the next column.
30L5row=3 queens=3 solutions=0Row 4, column 2: attacked by the queen at row 1, column 2. Try the next column.
31L6row=4 queens=4 solutions=0Row 4, column 3 is safe: place a queen.
32L2row=4 queens=4 solutions=1All 4 queens are placed safely: solution 1 is [2, 4, 1, 3] (column of each row, 1-based).
33L11row=4 queens=4 solutions=1 return=[2, 4, 1, 3]First solution: [2, 4, 1, 3]. 8 queens placed and 4 backtracks on the way.
20.03.2

Sudoku solver

Sudoku fits the same template: pick an empty cell, try each digit that does not clash with its row, column or box, recurse, undo. A candidate test with three sets (or bit masks) per row, column and box makes each check O(1). The animation solves the 4×4 version with a 2×2 box, so you can follow every step; the 9×9 code is identical except for the sizes.

The “Forced moves: 4 placements instead of 12” preset shows the effect of constraint propagation, which the lesson on search improvements covers. Try “Unsolvable” to see the search exhaust every branch and report failure.

Java
1static boolean solveSudoku(int[][] g) { // 0 = empty; returns true when solved
2 for (int r = 0; r < 9; r++)
3 for (int c = 0; c < 9; c++) {
4 if (g[r][c] != 0) continue;
5 for (int v = 1; v <= 9; v++) {
6 if (!legalDigit(g, r, c, v)) continue;
7 g[r][c] = v; // choose
8 if (solveSudoku(g)) return true; // explore
9 g[r][c] = 0; // unchoose
10 }
11 return false; // no digit fits this cell: dead end
12 }
13 return true; // no empty cell: solved
14}
15private static boolean legalDigit(int[][] g, int r, int c, int v) {
16 for (int k = 0; k < 9; k++) if (g[r][k] == v || g[k][c] == v) return false;
17 int br = r / 3 * 3, bc = c / 3 * 3;
18 for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) if (g[br + i][bc + j] == v) return false;
19 return true;
20}

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.03.4

Rat in a maze

A rat starts at the top-left of a grid of open cells and walls and wants the bottom-right. The search tries the four moves in a fixed order (Down, Left, Right, Up gives paths in alphabetical order), marks the current cell as visited, and unmarks it on the way back so other routes may pass through it. Visiting each cell at most once per path means the search finds *simple* paths, and the number of simple paths in an open grid grows very fast.

In the open 3×3 maze there are 12 simple paths from corner to corner, and the 2×2 maze has 2. The “One path” preset finds DRDR immediately. Note the difference from BFS: BFS finds one *shortest* path; backtracking enumerates *all* simple paths.

Java
1static void paths(int[][] m, int r, int c, StringBuilder path, List<String> out) { // 1 = open, 0 = wall
2 int n = m.length;
3 if (r == n - 1 && c == n - 1) { out.add(path.toString()); return; } // reached the exit
4 m[r][c] = 0; // choose: mark as visited
5 String dirs = "DLRU"; int[] dr = {1, 0, 0, -1}, dc = {0, -1, 1, 0};
6 for (int k = 0; k < 4; k++) {
7 int nr = r + dr[k], nc = c + dc[k];
8 if (nr < 0 || nc < 0 || nr >= n || nc >= n || m[nr][nc] == 0) continue; // outside, a wall, or already visited
9 path.append(dirs.charAt(k));
10 paths(m, nr, nc, path, out);
11 path.deleteCharAt(path.length() - 1);
12 }
13 m[r][c] = 1; // unchoose: free the cell
14}

Rat in a maze: all paths by backtracking

Interactive · edit the input

Your input

Try

grid · 3 × 3

012
0
•
▓
▓
1
▓
2
▓
★
On the current pathSolution pathBlockedWall
Setup

The rat starts at the top-left and wants the bottom-right (★). It tries the moves in the order Down, Left, Right, Up, never stepping on a wall or a cell it is already using. Stop at the first path.

Step 1 / 9
Java
1static void paths(int[][] m, int r, int c, StringBuilder path, List<String> out) { // 1 = open, 0 = wall
2 int n = m.length;
3 if (r == n - 1 && c == n - 1) { out.add(path.toString()); return; } // reached the exit
4 m[r][c] = 0; // choose: mark as visited
5 String dirs = "DLRU"; int[] dr = {1, 0, 0, -1}, dc = {0, -1, 1, 0};
6 for (int k = 0; k < 4; k++) {
7 int nr = r + dr[k], nc = c + dc[k];
8 if (nr < 0 || nc < 0 || nr >= n || nc >= n || m[nr][nc] == 0) continue; // outside, a wall, or already visited
9 path.append(dirs.charAt(k)); // choose the move
10 paths(m, nr, nc, path, out); // explore
11 path.deleteCharAt(path.length() - 1); // unchoose the move
12 }
13 m[r][c] = 1; // unchoose: free the cell
14}

Live state

0 moves0 backtracks

path

(start)

paths

0

No condition evaluated on this step
Execution trace (dry run table)Show 9 steps
StepLineStateWhat happens
1L1path=(start) paths=0The rat starts at the top-left and wants the bottom-right (★). It tries the moves in the order Down, Left, Right, Up, never stepping on a wall or a cell it is already using. Stop at the first path.
2L9path=D paths=0 moves=1Move D to (2, 1).
3L8path=D paths=0 moves=1D to (3, 1): a wall, skip.
4L9path=DR paths=0 moves=2Move R to (2, 2).
5L9path=DRD paths=0 moves=3Move D to (3, 2).
6L8path=DRD paths=0 moves=3L to (3, 1): a wall, skip.
7L9path=DRDR paths=0 moves=4Move R to (3, 3).
8L3path=DRDR paths=1 moves=4Reached the exit: path 1 is "DRDR".
9L14return=DRDRFirst path found: "DRDR".
20.03.5

Graph coloring

Colour the vertices of a graph with at most m colours so that adjacent vertices differ. Colour vertices in order; for vertex v try each colour not used by an already-coloured neighbour. When no colour fits, back up. Whether a graph is 3-colourable is NP-complete, so no polynomial algorithm is known; backtracking with pruning is the standard exact method.

The “Triangle plus a tail, 3 colours” preset finds [c0, c1, c2, c0] in 8 nodes. The “K4 with 3 colours: impossible” preset exhausts the search: four mutually adjacent vertices need four colours. Symmetry breaking (see search improvements) cuts that search from 49 to 12 nodes.

Java
1static boolean color(int[][] adj, int v, int[] colors, int m) { // colors[] starts filled with -1
2 if (v == adj.length) return true; // every vertex is coloured
3 for (int c = 0; c < m; c++) {
4 if (conflict(adj, v, c, colors)) continue; // a neighbour already has colour c
5 colors[v] = c; // choose
6 if (color(adj, v + 1, colors, m)) return true; // explore
7 colors[v] = -1; // unchoose
8 }
9 return false;
10}
11private static boolean conflict(int[][] adj, int v, int c, int[] colors) {
12 for (int u : adj[v]) if (colors[u] == c) return true;
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.

Practice ladder

Now try it yourself

Easy

  • N-Queens II (count only, n = 4)
  • Flood Fill

Medium

  • N-Queens
  • Word Search
  • Rat in a Maze

Advanced

  • Sudoku Solver
  • Word Search II
  • Unique Paths III
  • Graph Coloring (m-colouring problem)

Key takeaways

  • Constraint problems add a validity check to choose-explore-unchoose; encode the check in O(1).
  • Mark-and-restore is the unchoose for grid searches.
  • Colouring and Hamiltonian path are NP-complete: backtracking is exact but can be exponential.