dlx
April 22, 2026 · View on GitHub
Exact cover solver using Knuth's Algorithm DLX, exposed as DuckDB table functions.
Building
# 1. Clone this repo alongside the extension-template submodules
git clone --recurse-submodules https://github.com/jaripekkatalgraf/dlx-duckdb.git
cd dlx-duckdb
# 2. Optional but recommended: install ccache + ninja for fast rebuilds
# brew install ccache ninja (macOS)
# apt install ccache ninja-build (Ubuntu)
# 3. Build
GEN=ninja make
# 4. Test
make test
# 5. Start a DuckDB shell with the extension pre-loaded
./build/release/duckdb
Functions
dlx_solve(num_primary, rows [, num_secondary := 0] [, max_solutions := -1])
Returns TABLE(solution_id INTEGER, row_index INTEGER)
Solves the exact cover problem defined by:
num_primary INTEGER— number of primary columns (must be covered exactly once)rows LIST(LIST(INTEGER))— sparse row representation; each inner list is the set of column indices (0-based) that this row coversnum_secondary := INTEGER— optional secondary columns appended after the primary ones (covered at most once)max_solutions := BIGINT— stop after this many solutions
Each output row identifies one (solution, selected-input-row) pair. Join on solution_id to reconstruct full solutions.
dlx_count(num_primary, rows [, num_secondary := 0] [, max_solutions := -1])
Returns TABLE(solution_count BIGINT)
Counts solutions without materialising them (faster for large problems).
Examples
Knuth's paper example
SELECT solution_id, row_index
FROM dlx_solve(7, [
[2,4,5], -- row 0
[0,3,6], -- row 1
[1,2,3,4,5], -- row 2
[0,3], -- row 3
[1,2,5,6], -- row 4
[1,6] -- row 5
]);
-- solution_id │ row_index
-- ────────────┼──────────
-- 0 │ 0
-- 0 │ 3
-- 0 │ 5
4-Queens (generalized exact cover with secondary columns)
-- 8 primary (4 ranks + 4 files), 12 secondary (diagonals)
SELECT solution_count FROM dlx_count(8, [...], num_secondary := 12);
-- 2
Count solutions then enumerate only the first 10
-- Count first
SELECT solution_count FROM dlx_count(7, rows_expr);
-- Then enumerate with a cap
SELECT * FROM dlx_solve(7, rows_expr, max_solutions := 10);
Reconstruct solutions with original row data
WITH problem(row_idx, cols) AS (
VALUES (0, [2,4,5]), (1, [0,3,6]), (2, [1,2,3,4,5]),
(3, [0,3]), (4, [1,2,5,6]), (5, [3,4,6])
),
solutions AS (
SELECT * FROM dlx_solve(7, list(cols ORDER BY row_idx) OVER ())
)
SELECT s.solution_id, p.row_idx, p.cols
FROM solutions s
JOIN problem p ON p.row_idx = s.row_index
ORDER BY s.solution_id, p.row_idx;
Sudoku (sketch)
A standard 9×9 Sudoku encodes to an exact cover with 324 primary columns (81 cell + 81 row-digit + 81 col-digit + 81 box-digit constraints) and 729 rows (one per cell×digit). Build the row list with a helper macro and call:
SELECT row_index FROM dlx_solve(324, sudoku_rows(puzzle_string));
Algorithm notes
- MRV heuristic (
choose_column): always branches on the primary column with fewest remaining rows, minimising the search tree. - Generalised exact cover: secondary columns share no link with the root's horizontal list. They are covered when a selected row contains them, preventing double-coverage, but they are never required to be covered.
- Memory:
O(total nodes)=O(sum of row lengths). All nodes are stored in a contiguousstd::vectorfor cache locality. - Solutions collected upfront in
InitGlobal, then streamed inSTANDARD_VECTOR_SIZEchunks via the scan. This keeps the scan trivial and thread-safe.
File structure
src/
dlx_solver.hpp # Header-only Dancing Links implementation (no deps)
dlx_function.cpp # DuckDB table functions (dlx_solve, dlx_count)
dlx_extension.cpp # Extension entry point
include/
dlx_extension.hpp
test/sql/
dlx.test # sqllogictest suite
CMakeLists.txt
extension_config.cmake