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
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}.
| Row | Columns covered |
|---|---|
| A | 1, 4, 7 |
| B | 1, 4 |
| C | 4, 5, 7 |
| D | 3, 5, 6 |
| E | 2, 3, 6, 7 |
| F | 2, 7 |
Exact cover with Knuth's Algorithm X
Interactive · edit the inputgrid · 7 × 8
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.
1static void solve(Map<Integer, Set<String>> cols, Map<String, List<Integer>> rows, Deque<String> chosen, List<List<String>> out) {2if (cols.isEmpty()) { out.add(new ArrayList<>(chosen)); return; } // every column is covered exactly once3int c = Collections.min(cols.keySet(), Comparator.comparingInt(k -> cols.get(k).size())); // the column with the fewest rows4for (String r : new ArrayList<>(cols.get(c))) { // try each row that covers it5chosen.push(r); // choose row r6List<Set<String>> removed = select(cols, rows, r); // cover its columns, delete conflicting rows7solve(cols, rows, chosen, out); // explore8deselect(cols, rows, r, removed); // restore the matrix9chosen.pop(); // unchoose10}11}
Live state
chosen
[]
columns left
7
solutions
0
Execution trace (dry run table)Show 15 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | chosen=[] columns left=7 solutions=0 | 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. |
| 2 | L3 | chosen=[] columns left=7 solutions=0 | Column 1 has the fewest candidate rows (2: [A, B]). One of them must be chosen. |
| 3 | L6 | chosen=[A] columns left=4 solutions=0 | Choose row A: its columns [1, 4, 7] are now covered, so every row that shares one of them ([B, C, E, F]) is deleted. |
| 4 | L3 | chosen=[A] columns left=4 solutions=0 | Column 2 has the fewest candidate rows (0: none). Nothing covers it: dead end. |
| 5 | L8 | chosen=[] columns left=7 solutions=0 | Unchoose row A: restore its columns and the deleted rows. |
| 6 | L6 | chosen=[B] columns left=5 solutions=0 | Choose row B: its columns [1, 4] are now covered, so every row that shares one of them ([A, C]) is deleted. |
| 7 | L3 | chosen=[B] columns left=5 solutions=0 | Column 5 has the fewest candidate rows (1: [D]). One of them must be chosen. |
| 8 | L6 | chosen=[B, D] columns left=2 solutions=0 | Choose row D: its columns [3, 5, 6] are now covered, so every row that shares one of them ([E]) is deleted. |
| 9 | L3 | chosen=[B, D] columns left=2 solutions=0 | Column 2 has the fewest candidate rows (1: [F]). One of them must be chosen. |
| 10 | L6 | chosen=[B, D, F] columns left=0 solutions=0 | Choose row F: its columns [2, 7] are now covered, so every row that shares one of them (none) is deleted. |
| 11 | L2 | chosen=[B, D, F] columns left=0 solutions=1 | No column is left: the chosen rows [B, D, F] cover every column exactly once. Solution 1. |
| 12 | L8 | chosen=[B, D] columns left=2 solutions=1 | Unchoose row F: restore its columns and the deleted rows. |
| 13 | L8 | chosen=[B] columns left=5 solutions=1 | Unchoose row D: restore its columns and the deleted rows. |
| 14 | L8 | chosen=[] columns left=7 solutions=1 | Unchoose row B: restore its columns and the deleted rows. |
| 15 | L11 | solutions=1 return=[[B, D, F]] | 1 exact cover: [B, D, F]. |
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.
1static void algorithmX(Map<Integer, Set<String>> cols, Map<String, List<Integer>> rows, Deque<String> chosen, List<List<String>> out) {2if (cols.isEmpty()) { out.add(new ArrayList<>(chosen)); return; } // every column is covered exactly once3int c = Collections.min(cols.keySet(), Comparator.comparingInt((Integer k) -> cols.get(k).size())); // the column with the fewest rows4for (String r : new ArrayList<>(cols.get(c))) { // try each row that covers it5chosen.push(r); // choose row r6List<Set<String>> removed = select(cols, rows, r); // cover its columns, delete conflicting rows7algorithmX(cols, rows, chosen, out); // explore8deselect(cols, rows, r, removed); // restore the matrix9chosen.pop(); // unchoose10}11}12private static List<Set<String>> select(Map<Integer, Set<String>> cols, Map<String, List<Integer>> rows, String r) {13List<Set<String>> removed = new ArrayList<>();14for (int j : rows.get(r))15for (String i : cols.get(j))16for (int k : rows.get(i)) if (k != j) cols.get(k).remove(i); // row i conflicts: drop it from the other columns17for (int j : rows.get(r)) removed.add(cols.remove(j)); // remove the covered columns18return removed;19}20private static void deselect(Map<Integer, Set<String>> cols, Map<String, List<Integer>> rows, String r, List<Set<String>> removed) {21List<Integer> rc = rows.get(r);22for (int idx = rc.size() - 1; idx >= 0; idx--) {23int j = rc.get(idx);24cols.put(j, removed.remove(removed.size() - 1)); // put the column back25for (String i : cols.get(j))26for (int k : rows.get(i)) if (k != j) cols.get(k).add(i); // restore the dropped rows27}28}
Exact cover with Knuth's Algorithm X
Interactive · edit the inputgrid · 7 × 8
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.
1static void solve(Map<Integer, Set<String>> cols, Map<String, List<Integer>> rows, Deque<String> chosen, List<List<String>> out) {2if (cols.isEmpty()) { out.add(new ArrayList<>(chosen)); return; } // every column is covered exactly once3int c = Collections.min(cols.keySet(), Comparator.comparingInt(k -> cols.get(k).size())); // the column with the fewest rows4for (String r : new ArrayList<>(cols.get(c))) { // try each row that covers it5chosen.push(r); // choose row r6List<Set<String>> removed = select(cols, rows, r); // cover its columns, delete conflicting rows7solve(cols, rows, chosen, out); // explore8deselect(cols, rows, r, removed); // restore the matrix9chosen.pop(); // unchoose10}11}
Live state
chosen
[]
columns left
7
solutions
0
Execution trace (dry run table)Show 15 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L1 | chosen=[] columns left=7 solutions=0 | 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. |
| 2 | L3 | chosen=[] columns left=7 solutions=0 | Column 1 has the fewest candidate rows (2: [A, B]). One of them must be chosen. |
| 3 | L6 | chosen=[A] columns left=4 solutions=0 | Choose row A: its columns [1, 4, 7] are now covered, so every row that shares one of them ([B, C, E, F]) is deleted. |
| 4 | L3 | chosen=[A] columns left=4 solutions=0 | Column 2 has the fewest candidate rows (0: none). Nothing covers it: dead end. |
| 5 | L8 | chosen=[] columns left=7 solutions=0 | Unchoose row A: restore its columns and the deleted rows. |
| 6 | L6 | chosen=[B] columns left=5 solutions=0 | Choose row B: its columns [1, 4] are now covered, so every row that shares one of them ([A, C]) is deleted. |
| 7 | L3 | chosen=[B] columns left=5 solutions=0 | Column 5 has the fewest candidate rows (1: [D]). One of them must be chosen. |
| 8 | L6 | chosen=[B, D] columns left=2 solutions=0 | Choose row D: its columns [3, 5, 6] are now covered, so every row that shares one of them ([E]) is deleted. |
| 9 | L3 | chosen=[B, D] columns left=2 solutions=0 | Column 2 has the fewest candidate rows (1: [F]). One of them must be chosen. |
| 10 | L6 | chosen=[B, D, F] columns left=0 solutions=0 | Choose row F: its columns [2, 7] are now covered, so every row that shares one of them (none) is deleted. |
| 11 | L2 | chosen=[B, D, F] columns left=0 solutions=1 | No column is left: the chosen rows [B, D, F] cover every column exactly once. Solution 1. |
| 12 | L8 | chosen=[B, D] columns left=2 solutions=1 | Unchoose row F: restore its columns and the deleted rows. |
| 13 | L8 | chosen=[B] columns left=5 solutions=1 | Unchoose row D: restore its columns and the deleted rows. |
| 14 | L8 | chosen=[] columns left=7 solutions=1 | Unchoose row B: restore its columns and the deleted rows. |
| 15 | L11 | solutions=1 return=[[B, D, F]] | 1 exact cover: [B, D, F]. |
Dancing Links overview
Algorithm X deletes and restores rows and columns constantly. **Dancing Links (DLX)** stores the 0/1 matrix as a grid of nodes in which every node sits in two circular doubly linked lists, one across its row and one down its column, plus a header row of columns. Removing a node from a doubly linked list is x.left.right = x.right; x.right.left = x.left, and — the trick — the removed node still remembers its neighbours, so it can be re-inserted by the exact reverse: x.left.right = x; x.right.left = x. Undo is therefore O(1) per node with no copying.
cover(c) unlinks column header c and, for every row in column c, unlinks the other cells of that row from their columns. uncover(c) relinks them in the reverse order. The search loop is then a few lines. The class below solves exact cover and returns every solution as a list of row indices; it was checked against brute force on random matrices.
1final class Dlx {2private static class Node { Node l, r, u, d; Column c; int row; }3private static final class Column extends Node { int size; Column() { c = this; } }45private final Column head = new Column();6private final List<Integer> chosen = new ArrayList<>();7final List<List<Integer>> solutions = new ArrayList<>();89Dlx(int nCols, int[][] rows) { // rows[i] = the columns (0-based) that row i covers10head.l = head.r = head;11Column[] cols = new Column[nCols];12for (int j = 0; j < nCols; j++) {13Column col = new Column(); cols[j] = col;14col.u = col.d = col;15col.l = head.l; col.r = head; head.l.r = col; head.l = col; // append the header to the header row16}17for (int i = 0; i < rows.length; i++) {18Node first = null;19for (int j : rows[i]) {20Node n = new Node(); n.row = i; n.c = cols[j];21n.u = cols[j].u; n.d = cols[j]; cols[j].u.d = n; cols[j].u = n; cols[j].size++; // bottom of column j22if (first == null) { first = n; n.l = n.r = n; }23else { n.l = first.l; n.r = first; first.l.r = n; first.l = n; } // end of the row's circle24}25}26}2728void solve() { search(); }2930private void search() {31if (head.r == head) { solutions.add(new ArrayList<>(chosen)); return; } // no column left: exact cover32Column c = (Column) head.r;33for (Node k = c.r; k != head; k = k.r) if (((Column) k).size < c.size) c = (Column) k; // fewest rows34cover(c);35for (Node r = c.d; r != c; r = r.d) {36chosen.add(r.row); // choose row r37for (Node j = r.r; j != r; j = j.r) cover(j.c); // cover the other columns of r38search();39for (Node j = r.l; j != r; j = j.l) uncover(j.c); // undo in the opposite order40chosen.remove(chosen.size() - 1);41}42uncover(c);43}4445private static void cover(Column c) {46c.r.l = c.l; c.l.r = c.r; // unlink the column header47for (Node i = c.d; i != c; i = i.d)48for (Node j = i.r; j != i; j = j.r) { j.d.u = j.u; j.u.d = j.d; j.c.size--; } // unlink the rows that use this column49}50private static void uncover(Column c) {51for (Node i = c.u; i != c; i = i.u)52for (Node j = i.l; j != i; j = j.l) { j.c.size++; j.d.u = j; j.u.d = j; } // relink in reverse order53c.r.l = c; c.l.r = c;54}55}
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 family | Columns | Meaning |
|---|---|---|
| Cell (r, c) has a digit | 81 | index r·9 + c |
| Row r has digit d | 81 | index 81 + r·9 + (d−1) |
| Column c has digit d | 81 | index 162 + c·9 + (d−1) |
| Box b has digit d | 81 | index 243 + b·9 + (d−1) |
| Total | 324 | each of the 729 placements covers 4 of them |
1static int[] sudokuColumns(int r, int c, int d) { // d is 1..9; returns the four columns this placement covers2int box = r / 3 * 3 + c / 3;3return new int[]{4r * 9 + c, // cell (r, c) is filled581 + r * 9 + (d - 1), // row r contains d6162 + c * 9 + (d - 1), // column c contains d7243 + box * 9 + (d - 1) // box contains d8};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.