Chapter 20: Backtracking, Constraint Search and State Space
20.07Intermediate4 topics18 min2 interactive animations

Exact cover introduction

Exact cover: formulating problems as choosing rows of a 0/1 matrix, Knuth's Algorithm X, the Dancing Links implementation and Sudoku as an exact cover problem.

Learning goals

  • State the exact cover problem and convert a puzzle into a matrix
  • Trace Algorithm X, including the fewest-candidates column choice
  • Explain how doubly linked lists make cover and uncover O(1) per cell
  • Count the rows and columns of the Sudoku exact cover matrix

Prerequisites

Backtracking methodSearch improvementsLinked lists
20.07.1

Exact cover formulation

Given a set of columns (the *universe*) and a collection of rows (each a subset of the columns), an **exact cover** is a set of rows in which every column appears in **exactly one** chosen row. It is the problem behind tilings, Sudoku, N-Queens variants and scheduling puzzles, and it is NP-complete in general.

To model a puzzle, make one column per *constraint* that must be satisfied exactly once, and one row per *possible move* listing the constraints it satisfies. Choosing a set of moves that satisfies every constraint exactly once solves the puzzle. In Knuth's small example the universe is {1,…,7} and the rows are A = {1,4,7}, B = {1,4}, C = {4,5,7}, D = {3,5,6}, E = {2,3,6,7}, F = {2,7}; the only exact cover is {B, D, F}.

RowColumns covered
A1, 4, 7
B1, 4
C4, 5, 7
D3, 5, 6
E2, 3, 6, 7
F2, 7

Exact cover with Knuth's Algorithm X

Interactive · edit the input

Your input

Try

grid · 7 × 8

01234567
0
1
2
3
4
5
6
7
1
A
1
1
1
2
B
1
1
3
C
1
1
1
4
D
1
1
1
5
E
1
1
1
1
6
F
1
1
Column with the fewest candidatesRow being triedChosen rowRemoved from the matrix
Setup

Choose some of the 6 subsets (rows) so that every column 1-7 is covered exactly once. Algorithm X always branches on the column with the fewest candidate rows.

Step 1 / 15
Java
1static void solve(Map<Integer, Set<String>> cols, Map<String, List<Integer>> rows, Deque<String> chosen, List<List<String>> out) {
2 if (cols.isEmpty()) { out.add(new ArrayList<>(chosen)); return; } // every column is covered exactly once
3 int c = Collections.min(cols.keySet(), Comparator.comparingInt(k -> cols.get(k).size())); // the column with the fewest rows
4 for (String r : new ArrayList<>(cols.get(c))) { // try each row that covers it
5 chosen.push(r); // choose row r
6 List<Set<String>> removed = select(cols, rows, r); // cover its columns, delete conflicting rows
7 solve(cols, rows, chosen, out); // explore
8 deselect(cols, rows, r, removed); // restore the matrix
9 chosen.pop(); // unchoose
10 }
11}

Live state

0 rows tried0 dead ends

chosen

[]

columns left

7

solutions

0

No condition evaluated on this step
Execution trace (dry run table)Show 15 steps
StepLineStateWhat happens
1L1chosen=[] columns left=7 solutions=0Choose some of the 6 subsets (rows) so that every column 1-7 is covered exactly once. Algorithm X always branches on the column with the fewest candidate rows.
2L3chosen=[] columns left=7 solutions=0Column 1 has the fewest candidate rows (2: [A, B]). One of them must be chosen.
3L6chosen=[A] columns left=4 solutions=0Choose row A: its columns [1, 4, 7] are now covered, so every row that shares one of them ([B, C, E, F]) is deleted.
4L3chosen=[A] columns left=4 solutions=0Column 2 has the fewest candidate rows (0: none). Nothing covers it: dead end.
5L8chosen=[] columns left=7 solutions=0Unchoose row A: restore its columns and the deleted rows.
6L6chosen=[B] columns left=5 solutions=0Choose row B: its columns [1, 4] are now covered, so every row that shares one of them ([A, C]) is deleted.
7L3chosen=[B] columns left=5 solutions=0Column 5 has the fewest candidate rows (1: [D]). One of them must be chosen.
8L6chosen=[B, D] columns left=2 solutions=0Choose row D: its columns [3, 5, 6] are now covered, so every row that shares one of them ([E]) is deleted.
9L3chosen=[B, D] columns left=2 solutions=0Column 2 has the fewest candidate rows (1: [F]). One of them must be chosen.
10L6chosen=[B, D, F] columns left=0 solutions=0Choose row F: its columns [2, 7] are now covered, so every row that shares one of them (none) is deleted.
11L2chosen=[B, D, F] columns left=0 solutions=1No column is left: the chosen rows [B, D, F] cover every column exactly once. Solution 1.
12L8chosen=[B, D] columns left=2 solutions=1Unchoose row F: restore its columns and the deleted rows.
13L8chosen=[B] columns left=5 solutions=1Unchoose row D: restore its columns and the deleted rows.
14L8chosen=[] columns left=7 solutions=1Unchoose row B: restore its columns and the deleted rows.
15L11solutions=1 return=[[B, D, F]]1 exact cover: [B, D, F].
20.07.2

Algorithm X

**Algorithm X** is plain backtracking on the matrix. If no column is left, the chosen rows are a solution. Otherwise pick the column with the **fewest rows** (a column with no rows is an immediate dead end), try each row that covers it, and for a chosen row delete every column it covers together with every other row that shares one of those columns, since those rows would cover a column twice. Recurse, then restore the matrix.

Step through the first preset: column 2 or 3 has the fewest candidates, rows are tried in turn, and each choice shrinks the matrix. “No exact cover” shows a matrix where every branch ends in a dead end, and “Several solutions” lists all of them. The cost depends entirely on how fast deleting and restoring is, which is what Dancing Links optimises.

Java
1static void algorithmX(Map<Integer, Set<String>> cols, Map<String, List<Integer>> rows, Deque<String> chosen, List<List<String>> out) {
2 if (cols.isEmpty()) { out.add(new ArrayList<>(chosen)); return; } // every column is covered exactly once
3 int c = Collections.min(cols.keySet(), Comparator.comparingInt((Integer k) -> cols.get(k).size())); // the column with the fewest rows
4 for (String r : new ArrayList<>(cols.get(c))) { // try each row that covers it
5 chosen.push(r); // choose row r
6 List<Set<String>> removed = select(cols, rows, r); // cover its columns, delete conflicting rows
7 algorithmX(cols, rows, chosen, out); // explore
8 deselect(cols, rows, r, removed); // restore the matrix
9 chosen.pop(); // unchoose
10 }
11}
12private static List<Set<String>> select(Map<Integer, Set<String>> cols, Map<String, List<Integer>> rows, String r) {
13 List<Set<String>> removed = new ArrayList<>();
14 for (int j : rows.get(r))
15 for (String i : cols.get(j))
16 for (int k : rows.get(i)) if (k != j) cols.get(k).remove(i); // row i conflicts: drop it from the other columns
17 for (int j : rows.get(r)) removed.add(cols.remove(j)); // remove the covered columns
18 return removed;
19}
20private static void deselect(Map<Integer, Set<String>> cols, Map<String, List<Integer>> rows, String r, List<Set<String>> removed) {
21 List<Integer> rc = rows.get(r);
22 for (int idx = rc.size() - 1; idx >= 0; idx--) {
23 int j = rc.get(idx);
24 cols.put(j, removed.remove(removed.size() - 1)); // put the column back
25 for (String i : cols.get(j))
26 for (int k : rows.get(i)) if (k != j) cols.get(k).add(i); // restore the dropped rows
27 }
28}

Exact cover with Knuth's Algorithm X

Interactive · edit the input

Your input

Try

grid · 7 × 8

01234567
0
1
2
3
4
5
6
7
1
A
1
1
1
2
B
1
1
3
C
1
1
1
4
D
1
1
1
5
E
1
1
1
1
6
F
1
1
Column with the fewest candidatesRow being triedChosen rowRemoved from the matrix
Setup

Choose some of the 6 subsets (rows) so that every column 1-7 is covered exactly once. Algorithm X always branches on the column with the fewest candidate rows.

Step 1 / 15
Java
1static void solve(Map<Integer, Set<String>> cols, Map<String, List<Integer>> rows, Deque<String> chosen, List<List<String>> out) {
2 if (cols.isEmpty()) { out.add(new ArrayList<>(chosen)); return; } // every column is covered exactly once
3 int c = Collections.min(cols.keySet(), Comparator.comparingInt(k -> cols.get(k).size())); // the column with the fewest rows
4 for (String r : new ArrayList<>(cols.get(c))) { // try each row that covers it
5 chosen.push(r); // choose row r
6 List<Set<String>> removed = select(cols, rows, r); // cover its columns, delete conflicting rows
7 solve(cols, rows, chosen, out); // explore
8 deselect(cols, rows, r, removed); // restore the matrix
9 chosen.pop(); // unchoose
10 }
11}

Live state

0 rows tried0 dead ends

chosen

[]

columns left

7

solutions

0

No condition evaluated on this step
Execution trace (dry run table)Show 15 steps
StepLineStateWhat happens
1L1chosen=[] columns left=7 solutions=0Choose some of the 6 subsets (rows) so that every column 1-7 is covered exactly once. Algorithm X always branches on the column with the fewest candidate rows.
2L3chosen=[] columns left=7 solutions=0Column 1 has the fewest candidate rows (2: [A, B]). One of them must be chosen.
3L6chosen=[A] columns left=4 solutions=0Choose row A: its columns [1, 4, 7] are now covered, so every row that shares one of them ([B, C, E, F]) is deleted.
4L3chosen=[A] columns left=4 solutions=0Column 2 has the fewest candidate rows (0: none). Nothing covers it: dead end.
5L8chosen=[] columns left=7 solutions=0Unchoose row A: restore its columns and the deleted rows.
6L6chosen=[B] columns left=5 solutions=0Choose row B: its columns [1, 4] are now covered, so every row that shares one of them ([A, C]) is deleted.
7L3chosen=[B] columns left=5 solutions=0Column 5 has the fewest candidate rows (1: [D]). One of them must be chosen.
8L6chosen=[B, D] columns left=2 solutions=0Choose row D: its columns [3, 5, 6] are now covered, so every row that shares one of them ([E]) is deleted.
9L3chosen=[B, D] columns left=2 solutions=0Column 2 has the fewest candidate rows (1: [F]). One of them must be chosen.
10L6chosen=[B, D, F] columns left=0 solutions=0Choose row F: its columns [2, 7] are now covered, so every row that shares one of them (none) is deleted.
11L2chosen=[B, D, F] columns left=0 solutions=1No column is left: the chosen rows [B, D, F] cover every column exactly once. Solution 1.
12L8chosen=[B, D] columns left=2 solutions=1Unchoose row F: restore its columns and the deleted rows.
13L8chosen=[B] columns left=5 solutions=1Unchoose row D: restore its columns and the deleted rows.
14L8chosen=[] columns left=7 solutions=1Unchoose row B: restore its columns and the deleted rows.
15L11solutions=1 return=[[B, D, F]]1 exact cover: [B, D, F].
20.07.4

Sudoku exact cover

Sudoku as exact cover has **324 columns**, the four kinds of constraint, 81 each: every cell holds exactly one digit, every row contains each digit once, every column contains each digit once, every 3×3 box contains each digit once. It has **729 rows**: one for every (row, column, digit) placement, 9 × 9 × 9. Each row covers exactly four columns, one of each kind. A given digit is a row that must be chosen up front.

Choosing 81 rows that cover all 324 columns exactly once is exactly a filled, valid grid. Algorithm X picks the column with the fewest rows, which is the most constrained cell or digit placement, so it automatically does what the constraint-propagation lesson described by hand. Ordinary Sudoku puzzles are solved almost instantly this way.

Constraint familyColumnsMeaning
Cell (r, c) has a digit81index r·9 + c
Row r has digit d81index 81 + r·9 + (d−1)
Column c has digit d81index 162 + c·9 + (d−1)
Box b has digit d81index 243 + b·9 + (d−1)
Total324each of the 729 placements covers 4 of them
Java
1static int[] sudokuColumns(int r, int c, int d) { // d is 1..9; returns the four columns this placement covers
2 int box = r / 3 * 3 + c / 3;
3 return new int[]{
4 r * 9 + c, // cell (r, c) is filled
5 81 + r * 9 + (d - 1), // row r contains d
6 162 + c * 9 + (d - 1), // column c contains d
7 243 + box * 9 + (d - 1) // box contains d
8 };
9}

Practice ladder

Now try it yourself

Easy

  • Write the exact cover matrix for tiling a 2x2 board with dominoes
  • Why must a column with no rows end the branch?

Medium

  • Sudoku Solver (as exact cover)
  • Tiling a 2xN board with dominoes: count solutions

Advanced

  • Pentomino tiling
  • Implement Dancing Links and test it on N-Queens as an exact cover

Key takeaways

  • Exact cover: choose rows so that every column is covered exactly once.
  • Algorithm X branches on the column with the fewest candidate rows.
  • Dancing Links makes cover and uncover O(1) per cell by relinking doubly linked lists; Sudoku is a 729 × 324 matrix.