Working Notes: a commonplace notebook for recording & exploring ideas.
Home. Site Map. Subscribe. More at expLog. Kunal

Dancing Links & Algorithm X

Dancing Links came up while I was reading for real solutions to AoC'25 Day 12: if I actually wanted to solve the solutions. It's always a pleasure to read anything by Knuth.


While chatting with Gemini about the implementation of dancing links, I learned that Prof Knuth has covered the topic in Vol 4 Fascicle 5: which I happen to have.

It's fascinating to compare the paper (which feels very approachable and easy to understand) against the book: 500 problems on dancing links, with a 30 page description covering much more ground and types of problems.

The biggest change is that the book goes into a cache efficient implementation of dancing links for the linked list, to make it much easier to work with and explore; this is what I should try.


Follow up project ideas for later:


A very naive implementation of algorithm X, just because I was fascinated by the extremely clean description of backtracking (without any dancing links):

import logging
import sys

from logging import getLogger, basicConfig
from typing import NamedTuple


nl = logging.getLogger("naive")


def _pm(mat: list[list[int]]):
	if nl.isEnabledFor(logging.DEBUG):
		for i, row in enumerate(mat):
			print(" ".join(map(str, row)))


def _naive_soln_impl(amat: list[list[int]], soln: list[int]) -> list[int] | None:
	assert len(amat) > 0, "Must have annotation row/col"

	nl.debug(f"{soln=}")
	_pm(amat)

	#> If A is empty, the problem is solved; terminate successfully.
	if len(amat) == 1 and len(amat[0]) == 1:  # Just the corner
		return soln

	arows = len(amat)
	acols = len(amat[0])

	#> Otherwise choose a column, c (deterministically).
	min_branches: None | tuple[int, int] = None
	for j in range(1, acols):
		col_branches = sum(amat[i][j] for i in range(1, arows))
		if min_branches is None or col_branches < min_branches[1]:
			min_branches = (j, col_branches)

	nl.debug(f"{min_branches=}")
	if not min_branches or min_branches[1] == 0:
		return None
	c = min_branches[0]

	#> Choose a row, r, such that A[r, c] = 1 (nondeterministically).
	for r in range(1, arows):
		if amat[r][c] != 1:
			continue

		#> Include r in the partial solution.
		r_soln = [*soln, amat[r][0]]

		del_cols = set()
		del_rows = set()

		#> For each j such that A[r, j] = 1,
		for j in range(1, acols):
			if amat[r][j] != 1:
				continue

			#> delete column j from matrix A;
			del_cols.add(j)

			#> for each i such that A[i, j] = 1, delete row i from matrix A.
			for i in range(1, arows):
				if amat[i][j] == 1:
					del_rows.add(i)

		next_mat = []
		for i in range(0, arows):
			if i in del_rows:
				continue

			next_row = []
			for j in range(0, acols):
				if j in del_cols:
					continue
				next_row.append(amat[i][j])

			next_mat.append(next_row)

		#> Repeat this algorithm recursively on the reduced matrix A.
		if result := _naive_soln_impl(next_mat, r_soln):
			return result

	return None


def naive_soln(mat: list[list[int]]) -> list[int] | None:
	"""
	First attempt at implementing algorithm X without dancing links.
	"""

	# Add labels ot the matrix
	rows = len(mat)
	cols = len(mat[0])
	amat = [
		list(range(cols + 1)),
		*([i + 1, *row] for i, row in enumerate(mat)),
	]

	result = _naive_soln_impl(amat, list())
	return result