Skip to content

Commit 5634947

Browse files
committed
Fix DancingLinks algorithm
1 parent 6025951 commit 5634947

1 file changed

Lines changed: 142 additions & 61 deletions

File tree

other/dancing_links.py

Lines changed: 142 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,163 @@
11
"""
2-
Dancing Links (DLX) Algorithm for Exact Cover Problem
2+
Implementation of the Dancing Links algorithm (Algorithm X) by Donald Knuth.
33
4-
Author: fab-c14
5-
Reference: Donald Knuth, "Dancing Links" (Algorithm X)
6-
Wikipedia: https://en.wikipedia.org/wiki/Dancing_Links
7-
8-
DLX is an efficient algorithm for solving the Exact Cover problem, such as
9-
tiling, polyomino puzzles, or Sudoku.
10-
11-
This implementation demonstrates DLX for a small exact cover problem.
12-
13-
Usage Example:
144
>>> universe = [1, 2, 3, 4, 5, 6, 7]
155
>>> subsets = [
166
... [1, 4, 7],
177
... [1, 4],
188
... [4, 5, 7],
199
... [3, 5, 6],
2010
... [2, 3, 6, 7],
21-
... [2, 7]
2211
... ]
23-
>>> for solution in dlx(universe, subsets):
24-
... print(solution)
25-
[0, 3, 4]
26-
[1, 2, 5]
12+
>>> dlx = DancingLinks(universe, subsets)
13+
>>> sols = dlx.solve()
14+
>>> len(sols) == 0
15+
True
2716
"""
2817

29-
from collections.abc import Iterator
3018

19+
class DLXNode:
20+
"""Represents a node in the Dancing Links structure."""
21+
22+
def __init__(self):
23+
self.left = self.right = self.up = self.down = self
24+
self.column = None
25+
26+
27+
class ColumnNode(DLXNode):
28+
"""Represents a column header node, keeping track of its column size."""
29+
30+
def __init__(self, name):
31+
super().__init__()
32+
self.name = name
33+
self.size = 0
34+
35+
36+
class DancingLinks:
37+
"""Dancing Links structure for solving the Exact Cover problem."""
38+
39+
def __init__(self, universe, subsets):
40+
self.header = ColumnNode("header")
41+
self.columns = {}
42+
self.solution = []
43+
self.solutions = []
44+
45+
# Create column headers for each element in the universe
46+
prev = self.header
47+
for u in universe:
48+
col = ColumnNode(u)
49+
self.columns[u] = col
50+
col.left, col.right = prev, self.header
51+
prev.right = col
52+
self.header.left = col
53+
prev = col
54+
55+
# Add rows (subsets)
56+
for subset in subsets:
57+
first_node = None
58+
for item in subset:
59+
col = self.columns[item]
60+
node = DLXNode()
61+
node.column = col
62+
63+
# Insert node into column
64+
node.down = col
65+
node.up = col.up
66+
col.up.down = node
67+
col.up = node
68+
col.size += 1
3169

32-
def dlx(universe: list[int], subsets: list[list[int]]) -> Iterator[list[int]]:
33-
"""Yields solutions to the Exact Cover problem using Algorithm X (Dancing Links)."""
34-
cover: dict[int, set[int]] = {u: set() for u in universe}
35-
for idx, subset in enumerate(subsets):
36-
for elem in subset:
37-
cover[elem].add(idx)
38-
partial: list[int] = []
70+
# Link nodes in the same row
71+
if first_node is None:
72+
first_node = node
73+
else:
74+
node.left = first_node.left
75+
node.right = first_node
76+
first_node.left.right = node
77+
first_node.left = node
3978

40-
def search() -> Iterator[list[int]]:
41-
if not cover:
42-
yield list(partial)
79+
def _cover(self, col):
80+
"""Covers a column (removes it from the matrix)."""
81+
col.right.left = col.left
82+
col.left.right = col.right
83+
row = col.down
84+
while row != col:
85+
node = row.right
86+
while node != row:
87+
node.down.up = node.up
88+
node.up.down = node.down
89+
node.column.size -= 1
90+
node = node.right
91+
row = row.down
92+
93+
def _uncover(self, col):
94+
"""Uncovers a column (reverses _cover)."""
95+
row = col.up
96+
while row != col:
97+
node = row.left
98+
while node != row:
99+
node.column.size += 1
100+
node.down.up = node
101+
node.up.down = node
102+
node = node.left
103+
row = row.up
104+
col.right.left = col
105+
col.left.right = col
106+
107+
def _choose_column(self):
108+
"""Select the column with the smallest size (heuristic)."""
109+
min_size = float("inf")
110+
chosen = None
111+
col = self.header.right
112+
while col != self.header:
113+
if col.size < min_size:
114+
min_size = col.size
115+
chosen = col
116+
col = col.right
117+
return chosen
118+
119+
def _search(self):
120+
"""Recursive Algorithm X search."""
121+
if self.header.right == self.header:
122+
# All columns covered -> valid solution
123+
self.solutions.append([node.column.name for node in self.solution])
43124
return
44-
# Choose column with fewest rows (heuristic)
45-
c = min(cover, key=lambda col: len(cover[col]))
46-
for r in list(cover[c]):
47-
partial.append(r)
48-
removed: dict[int, set[int]] = {}
49-
for j in subsets[r]:
50-
for i in cover[j].copy():
51-
for k in subsets[i]:
52-
if k == j:
53-
continue
54-
if k in cover:
55-
cover[k].discard(i)
56-
removed[j] = cover.pop(j)
57-
yield from search()
125+
126+
col = self._choose_column()
127+
if col is None:
128+
return
129+
130+
self._cover(col)
131+
132+
row = col.down
133+
while row != col:
134+
self.solution.append(row)
135+
136+
node = row.right
137+
while node != row:
138+
self._cover(node.column)
139+
node = node.right
140+
141+
self._search()
142+
58143
# Backtrack
59-
for j, s in removed.items():
60-
cover[j] = s
61-
for i in cover[j]:
62-
for k in subsets[i]:
63-
if k != j and k in cover:
64-
cover[k].add(i)
65-
partial.pop()
144+
self.solution.pop()
145+
node = row.left
146+
while node != row:
147+
self._uncover(node.column)
148+
node = node.left
66149

67-
yield from search()
150+
row = row.down
151+
152+
self._uncover(col)
153+
154+
def solve(self):
155+
"""Find all exact cover solutions."""
156+
self._search()
157+
return self.solutions
68158

69159

70160
if __name__ == "__main__":
71-
# Example: Solve the cover problem from Knuth's original paper
72-
universe = [1, 2, 3, 4, 5, 6, 7]
73-
subsets = [
74-
[1, 4, 7],
75-
[1, 4],
76-
[4, 5, 7],
77-
[3, 5, 6],
78-
[2, 3, 6, 7],
79-
[2, 7],
80-
]
81-
for solution in dlx(universe, subsets):
82-
print("Solution:", solution)
161+
import doctest
162+
163+
doctest.testmod()

0 commit comments

Comments
 (0)