Skip to content

Commit 78d3358

Browse files
fab-c14cclauss
andauthored
Add Dancing Links (DLX) algorithm for Exact Cover problem (#13183)
* Add Dancing Links (DLX) algorithm for Exact Cover problem * Fix DancingLinks algorithm * Add Wikipedia links for Algorithm X and Dancing Links Added references to Wikipedia for Algorithm X and Dancing Links. * Add type hints to Dancing Links classes and methods --------- Co-authored-by: Christian Clauss <cclauss@me.com>
1 parent a2a2434 commit 78d3358

1 file changed

Lines changed: 165 additions & 0 deletions

File tree

other/dancing_links.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
"""
2+
Implementation of the Dancing Links algorithm (Algorithm X) by Donald Knuth.
3+
https://en.wikipedia.org/wiki/Knuth's_Algorithm_X
4+
https://en.wikipedia.org/wiki/Dancing_links
5+
6+
>>> universe = [1, 2, 3, 4, 5, 6, 7]
7+
>>> subsets = [
8+
... [1, 4, 7],
9+
... [1, 4],
10+
... [4, 5, 7],
11+
... [3, 5, 6],
12+
... [2, 3, 6, 7],
13+
... ]
14+
>>> dlx = DancingLinks(universe, subsets)
15+
>>> sols = dlx.solve()
16+
>>> len(sols) == 0
17+
True
18+
"""
19+
20+
21+
class DLXNode:
22+
"""Represents a node in the Dancing Links structure."""
23+
24+
def __init__(self) -> None:
25+
self.left = self.right = self.up = self.down = self
26+
self.column = None
27+
28+
29+
class ColumnNode(DLXNode):
30+
"""Represents a column header node, keeping track of its column size."""
31+
32+
def __init__(self, name: str) -> None:
33+
super().__init__()
34+
self.name = name
35+
self.size = 0
36+
37+
38+
class DancingLinks:
39+
"""Dancing Links structure for solving the Exact Cover problem."""
40+
41+
def __init__(self, universe: list[int], subsets: list[list[int]]) -> None:
42+
self.header = ColumnNode("header")
43+
self.columns = {}
44+
self.solution = []
45+
self.solutions = []
46+
47+
# Create column headers for each element in the universe
48+
prev = self.header
49+
for u in universe:
50+
col = ColumnNode(u)
51+
self.columns[u] = col
52+
col.left, col.right = prev, self.header
53+
prev.right = col
54+
self.header.left = col
55+
prev = col
56+
57+
# Add rows (subsets)
58+
for subset in subsets:
59+
first_node = None
60+
for item in subset:
61+
col = self.columns[item]
62+
node = DLXNode()
63+
node.column = col
64+
65+
# Insert node into column
66+
node.down = col
67+
node.up = col.up
68+
col.up.down = node
69+
col.up = node
70+
col.size += 1
71+
72+
# Link nodes in the same row
73+
if first_node is None:
74+
first_node = node
75+
else:
76+
node.left = first_node.left
77+
node.right = first_node
78+
first_node.left.right = node
79+
first_node.left = node
80+
81+
def _cover(self, col: ColumnNode) -> None:
82+
"""Covers a column (removes it from the matrix)."""
83+
col.right.left = col.left
84+
col.left.right = col.right
85+
row = col.down
86+
while row != col:
87+
node = row.right
88+
while node != row:
89+
node.down.up = node.up
90+
node.up.down = node.down
91+
node.column.size -= 1
92+
node = node.right
93+
row = row.down
94+
95+
def _uncover(self, col: ColumnNode):
96+
"""Uncovers a column (reverses _cover)."""
97+
row = col.up
98+
while row != col:
99+
node = row.left
100+
while node != row:
101+
node.column.size += 1
102+
node.down.up = node
103+
node.up.down = node
104+
node = node.left
105+
row = row.up
106+
col.right.left = col
107+
col.left.right = col
108+
109+
def _choose_column(self) -> ColumnNode:
110+
"""Select the column with the smallest size (heuristic)."""
111+
min_size = float("inf")
112+
chosen = None
113+
col = self.header.right
114+
while col != self.header:
115+
if col.size < min_size:
116+
min_size = col.size
117+
chosen = col
118+
col = col.right
119+
return chosen
120+
121+
def _search(self) -> None:
122+
"""Recursive Algorithm X search."""
123+
if self.header.right == self.header:
124+
# All columns covered -> valid solution
125+
self.solutions.append([node.column.name for node in self.solution])
126+
return
127+
128+
col = self._choose_column()
129+
if col is None:
130+
return
131+
132+
self._cover(col)
133+
134+
row = col.down
135+
while row != col:
136+
self.solution.append(row)
137+
138+
node = row.right
139+
while node != row:
140+
self._cover(node.column)
141+
node = node.right
142+
143+
self._search()
144+
145+
# Backtrack
146+
self.solution.pop()
147+
node = row.left
148+
while node != row:
149+
self._uncover(node.column)
150+
node = node.left
151+
152+
row = row.down
153+
154+
self._uncover(col)
155+
156+
def solve(self) -> list:
157+
"""Find all exact cover solutions."""
158+
self._search()
159+
return self.solutions
160+
161+
162+
if __name__ == "__main__":
163+
import doctest
164+
165+
doctest.testmod()

0 commit comments

Comments
 (0)