Skip to content

Commit 924ddf8

Browse files
committed
Implemented Knuth's AlgorithmX that solves the Exact Cover problem
1 parent 9cc05d7 commit 924ddf8

File tree

1 file changed

+94
-0
lines changed

1 file changed

+94
-0
lines changed

ExactCoverSolver.py

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from DancingLinks import *
2+
3+
class ExactCoverSolver:
4+
def __init__(self, problem):
5+
"""
6+
ExactCoverSolver takes an instance of an Exact Cover problem
7+
in the form of a sparse Dancing Links Matrix
8+
and solves it using Knuth's algorithmX
9+
10+
"""
11+
self.problem = problem
12+
self.backtrack_solution_trace = {}
13+
self.solution = {}
14+
self.list_of_solution_rows = []
15+
16+
def select_column(self):
17+
current_column_head_cell = self.problem.root.R
18+
best_column = current_column_head_cell
19+
20+
while current_column_head_cell != self.problem.root:
21+
if current_column_head_cell.size < best_column.size:
22+
best_column = current_column_head_cell
23+
current_column_head_cell = current_column_head_cell.R
24+
return best_column
25+
26+
def create_solution(self, d):
27+
"""
28+
We construct the final solution based on what we saved in the backtracking solution trace
29+
"""
30+
for k, row in self.backtrack_solution_trace.items():
31+
if k >= d:
32+
continue
33+
row_column_list = [row.C.name]
34+
row_column_list.extend(r.C.name for r in self.problem.iterate_cells(row, 'R'))
35+
row_name = self.problem.row_number_to_row_name[row.row_number]
36+
self.solution[row_name] = row_column_list
37+
self.list_of_solution_rows.append(row_name)
38+
39+
def search_helper(self, d):
40+
"""
41+
This is main backtracking method that solves the exact cover problem
42+
Each column has to have only one selected solution row in which it is set to 1
43+
So in each recursive step of the algorithm:
44+
We select a column c
45+
We cover c so that it will not be considered anymore
46+
Now we try all rows r in which c is set to 1:
47+
We select r as a part of the solution
48+
We cover all columns j in which r is set to 1 because by picking we already satisfy j
49+
We recurse
50+
If that did not work we uncover all column that we have covered and try next r
51+
"""
52+
if self.problem.root.R == self.problem.root:
53+
self.create_solution(d)
54+
return
55+
c = self.select_column()
56+
self.problem.cover(c)
57+
for r in self.problem.iterate_cells(c, 'D'):
58+
self.backtrack_solution_trace[d] = r
59+
for j in self.problem.iterate_cells(r, 'R'):
60+
self.problem.cover(j)
61+
self.search_helper(d + 1)
62+
for j in self.problem.iterate_cells(r, 'L'):
63+
self.problem.uncover(j)
64+
self.problem.uncover(c)
65+
66+
def algorithmX(self):
67+
"""
68+
Method to be called in order to solve the problem instance
69+
Return the list of row names to be selected and a dictionary of list that correspond to the values of those rows
70+
If the problem is unsolvable we will get an empty list and an empty dictionary
71+
"""
72+
self.search_helper(0)
73+
return self.list_of_solution_rows, self.solution
74+
75+
if __name__ == '__main__':
76+
columns = 7
77+
rows = [
78+
{"row_name": "1" , "row_value" : [2, 4, 5]},
79+
{"row_name": "2" , "row_value" : [0, 3, 6]},
80+
{"row_name": "3" , "row_value" : [1, 2, 5]},
81+
{"row_name": "4" , "row_value" : [0, 3]},
82+
{"row_name": "5" , "row_value" : [1, 6]},
83+
{"row_name": "6" , "row_value" : [3, 4, 6]},
84+
]
85+
86+
dlx = DLXMatrix(columns)
87+
for row in rows:
88+
dlx.add_sparse_row(row)
89+
print(dlx)
90+
solver = ExactCoverSolver(dlx)
91+
list_rows, full_solution = solver.algorithmX()
92+
for a, v in full_solution.items():
93+
print(a, v)
94+
print(list_rows)

0 commit comments

Comments
 (0)