Exact Cover

Given a universe and a family of subsets, choose a subfamily so that every element is covered exactly once. NP-complete (one of Karp’s 21). Equivalently: given a 0/1 matrix, select rows so that every column contains exactly one 1.

Algorithm X

Knuth’s backtracking recipe, with two decisions that make it fast:

AlgorithmX(matrix):
    if no columns remain: report the solution
    c = column with the fewest 1s                 (the S-heuristic: minimum-remaining-values)
    if c has no 1s: backtrack (dead end)
    for each row r with a 1 in c:
        add r to the solution
        cover every column where r has a 1 (removing conflicting rows from all of them)
        recurse
        uncover, remove r
  1. Branch on the most-constrained column (fewest 1s) — classic MRV, collapses the search tree enormously.
  2. Undo in using Dancing Links (DLX), where removing and restoring a doubly linked node are both two pointer writes.

Modelling — the actual skill

Columns are constraints, rows are choices.

Each row must have a 1 in exactly the constraints its choice satisfies.

Sudoku — 324 columns, 729 rows

Constraint groupColumnsMeaning
Cell81cell is filled once
Row81digit appears once in row
Column81digit appears once in column
Box81digit appears once in box

Each row is “place digit at ” with exactly four 1s. DLX solves the hardest known puzzles in microseconds.

N-Queens

  • Primary columns (exactly once): each rank, each file.
  • Secondary columns (at most once): each diagonal.

Secondary columns are the “exact cover with optional columns” generalisation — they may be left uncovered.

Polyomino tiling

  • One column per board square (must be covered once).
  • One column per piece (must be used once, if pieces are distinct).
  • One row per legal placement (position x orientation).

This is the canonical DLX application, and it solves pentomino packings that resist ad-hoc search.

Other encodings

ProblemColumnsRows
Latin squarescell, row-symbol, column-symbolplace symbol at cell
Perfect matchingeach vertexeach edge
Set partitioningeach elementeach candidate set
Langford pairseach position, each numbereach placement
Instant Insanityeach cube, each face-colour constrainteach orientation

Exact cover vs set cover

Exact coverSet cover
Requirementeach element exactly onceeach element at least once
Objectivefeasibility (or count)minimise the number of sets
Approximationnot meaningful greedy, and that is optimal
MethodDLX / Algorithm Xgreedy, ILP

They look similar and behave completely differently. Exact cover is a constraint satisfaction problem; set cover is an optimisation problem.

Variants

  • With optional (secondary) columns — “at most once” constraints, as in N-Queens diagonals.
  • XCC (exact cover with colours) — Knuth’s TAOCP 4B extension, where secondary columns carry a colour and multiple rows may share one if the colours agree. Handles puzzles with compatibility constraints.
  • Multiplicities — a column must be covered between and times.
  • Counting solutions rather than finding one — the same search, no early exit.

When to use DLX

Signal
The problem is naturally “cover every requirement exactly once”yes
Choices are enumerable and each satisfies a fixed small set of constraintsyes
Puzzle-like: tiling, placement, fillingyes
The constraint matrix is sparseyes
It is an optimisation, not a feasibility, problemno — use ILP or branch and bound
The constraints are boolean clausesno — use SAT

For contest use, a hand-written backtracking search with MRV usually suffices; DLX earns its place when the same solver must handle many instances or the naive search is too slow.

The data structure (Knuth, “DLX”) that makes Algorithm X fast: a sparse 0/1 matrix stored as a toroidal doubly linked list, in which removing and restoring a node are both .

The core trick

In a doubly linked list, removing node sets x.left.right = x.right and x.right.left = x.left. But itself still points at its old neighbours — so restoring it is just x.left.right = x and x.right.left = x. No allocation, no stack of saved values, no copying. Knuth called the effect “dancing” because the pointers seem to leap out and back.

Array-based DLX — the practical version

Six parallel int arrays (L,R,U,D,C) indexed by node id, plus a per-column size, instead of allocated pointer nodes: same algorithm, far better cache behaviour, no allocator pressure — this is what competitive implementations do.

struct DLX {
    vector<int> L, R, U, D, C, rowOf;
    vector<int> size;
    int nCols;
 
    DLX(int nCols_) : nCols(nCols_) {
        int total = nCols + 1;                     // node 0 is the root; nodes 1..nCols are column headers
        L.resize(total); R.resize(total); U.resize(total); D.resize(total); C.resize(total); rowOf.assign(total, -1);
        size.assign(nCols + 1, 0);
        for (int i = 0; i <= nCols; i++) { L[i] = i - 1; R[i] = i + 1; U[i] = D[i] = i; C[i] = i; }
        L[0] = nCols; R[nCols] = 0;
    }
 
    void addRow(int rowId, const vector<int>& cols) {   // cols: 1-indexed column ids, this row's 1s
        int first = -1, prev = -1;
        for (int c : cols) {
            int node = L.size();
            L.push_back(0); R.push_back(0); U.push_back(U[c]); D.push_back(c); C.push_back(c); rowOf.push_back(rowId);
            D[U[c]] = node; U[c] = node;
            size[c]++;
            if (first == -1) first = node;
            if (prev != -1) { L[node] = prev; R[prev] = node; }
            prev = node;
        }
        if (first != -1) { L[first] = prev; R[prev] = first; }
    }
 
    void cover(int c) {
        R[L[c]] = R[c]; L[R[c]] = L[c];
        for (int i = D[c]; i != c; i = D[i])
            for (int j = R[i]; j != i; j = R[j]) { U[D[j]] = U[j]; D[U[j]] = D[j]; size[C[j]]--; }
    }
    void uncover(int c) {
        for (int i = U[c]; i != c; i = U[i])
            for (int j = L[i]; j != i; j = L[j]) { size[C[j]]++; U[D[j]] = j; D[U[j]] = j; }
        R[L[c]] = c; L[R[c]] = c;
    }
 
    vector<int> sol;
    bool search() {                                 // finds one solution; sol holds the chosen row ids
        if (R[0] == 0) return true;                 // no columns left: every constraint covered
        int c = R[0];
        for (int j = R[0]; j != 0; j = R[j]) if (size[j] < size[c]) c = j;
        if (size[c] == 0) return false;              // dead end: an uncoverable column
        cover(c);
        for (int r = D[c]; r != c; r = D[r]) {
            sol.push_back(rowOf[r]);
            for (int j = R[r]; j != r; j = R[j]) cover(C[j]);
            if (search()) return true;
            for (int j = L[r]; j != r; j = L[j]) uncover(C[j]);
            sol.pop_back();
        }
        uncover(c);
        return false;
    }
};

Complexity: cover/uncover are , no hidden constant; the overall search is exponential in the worst case (exact cover is NP-complete), but the S-heuristic keeps real instances fast — a hard Sudoku in well under a millisecond, pentomino packings in seconds.

Tested: DLX checked against brute-force exact cover search (try every subset of rows, ) on small random 0/1 matrices, including matrices with no solution, exactly one, and several; and used to solve a full Sudoku puzzle (729 rows, 324 columns, as in the modelling table above) with the result cross-checked against the backtracking Sudoku solver’s answer.

Variants / Use Cases

  • Sudoku solvers — the canonical demo; solves the hardest known puzzles in microseconds.
  • Polyomino / pentomino tiling, N-Queens, Latin squares, Langford pairs.
  • XCC (exact cover with colours) — Knuth’s extension in TAOCP Volume 4B, for puzzles with compatibility constraints.
  • Any backtracking with heavy undo — the dancing-links idea generalises to rollback DSU, persistent structures, and undo-able segment trees.
  • Set cover — the minimisation cousin; NP-hard to approximate better than , see greedy approximations.