forked from neeru1207/AI_Sudoku
-
Notifications
You must be signed in to change notification settings - Fork 1
/
SudokuSolver.py
239 lines (214 loc) · 7.72 KB
/
SudokuSolver.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
from operator import attrgetter
class Solver:
def checkvalidpuzzle(self, arr):
subsquarestartingpoints = [[0, 0], [0, 3], [0, 6], [3, 0], [3, 3], [3, 6], [6, 0], [6, 3], [6, 6]]
# Checking row validity of every row
for row in range(9):
has = set()
for col in range(9):
if arr[row][col] == 0:
continue
if arr[row][col] in has:
return False
has.add(arr[row][col])
# Checking column validity of every column
for col in range(9):
has = set()
for row in range(9):
if arr[row][col] == 0:
continue
if arr[row][col] in has:
return False
has.add(arr[row][col])
# Checking box validity
for pointrow, pointcol in subsquarestartingpoints:
has = set()
for row in range(3):
for col in range(3):
if arr[pointrow+row][pointcol+col] == 0:
continue
if arr[pointrow+row][pointcol+col] in has:
return False
has.add(arr[pointrow+row][pointcol+col])
return True
def print_board(self, arr):
for i in range(9):
for j in range(9):
if arr[i][j]==0:
print("_", end=" ")
else:
print(arr[i][j], end=" ")
print("")
@staticmethod
def solve_sudoku(arr):
"""
Create a binary matrix to convert to an exact cover problem.
Choices: 729
Each cell can have any value from 1 to 9.
Constraints: 324
1. Each row must have all the values from 1 to 9, total: 81
2. Each column must have all the values from 1 to 9, total: 81
3. Each block must have all the values from 1 to 9, total: 81
4. Each cell must be filled, total: 81
Choices are ordered by row > col > value
Constraints are ordered as above.
"""
# Represent the binary matrix as sparse matrix (has < 729 * 4 ones in a matrix of 729 * 342)
positions = []
def add_position(ch, r, c, x):
positions.append([ch, [
9 * r + x, # Row constraint
81 + 9 * c + x, # Col constraint
162 + 9 * ((r // 3) * 3 + (c // 3)) + x, # Block constraint
243 + 9 * r + c # Cell constraint
]])
choice_row = 0
for i in range(9): # Row
for j in range(9): # Column
if arr[i][j] == 0:
for k in range(9): # Value
add_position(choice_row, i, j, k)
choice_row += 1
else:
k = arr[i][j] - 1
add_position(choice_row + k, i, j, k)
choice_row += 9
alg_x = AlgorithmX(324, positions)
if not alg_x.solve():
return False
rows = alg_x.solution
if len(rows) != 81:
return False
for row in rows:
i, row = divmod(row, 81)
j, value = divmod(row, 9)
arr[i][j] = value + 1 # value is 0-8
return True
class AlgorithmXNode:
def __init__(self, value=0):
"""
Create a node with self links.
:param value: Serves multiple purposes:
- nothing for root node
- the number of cells in column for all header nodes
- the row id in all other nodes
"""
self.value = value
self.left = self.right = self.up = self.down = self.top = self
def insert_h(self):
"""
Insert this node in the row, using left and right links.
"""
self.left.right = self.right.left = self
def insert_v(self, update_top=True):
"""
Insert this node in the column.
:param update_top: If true, update the counter in the header.
"""
self.up.down = self.down.up = self
if update_top:
self.top.value += 1
def insert_above(self, node):
"""
Insert this node above the given node, in the column, updating the top.
"""
self.top = node.top
self.up = node.up
self.down = node
self.insert_v()
def insert_after(self, node):
"""
Insert this node to the right the given node.
"""
self.right = node.right
self.left = node
self.insert_h()
def remove_h(self):
"""
Remove this node from the row. Inverse of insert_h.
"""
self.left.right = self.right
self.right.left = self.left
def remove_v(self, update_top=True):
"""
Remove this node from the column. Inverse of insert_v.
:param update_top: If true, update the counter in the header.
"""
self.up.down = self.down
self.down.up = self.up
if update_top:
self.top.value -= 1
def cover(self):
self.top.remove_h()
for row in self.top.loop('down'):
for node in row.loop('right'):
node.remove_v()
def uncover(self):
for row in self.top.loop('up'):
for node in row.loop('left'):
node.insert_v()
self.top.insert_h()
def loop(self, direction):
"""
Yield each node from self to self, following the direction, excluding self.
:param direction: One of 'left', 'right', 'up', 'down'.
:return: Nodes from self to self (both exclusive), one at a time.
"""
if direction not in {'left', 'right', 'up', 'down'}:
raise ValueError(f"Direction must be one of 'left', 'right', 'up', 'down', got {direction}")
next_node = attrgetter(direction)
node = next_node(self)
while node != self:
yield node
node = next_node(node)
class AlgorithmX:
"""
Use Algorithm X with dancing links to solve a constraint satisfaction problem
represented in the form of Exact Cover.
Refer to https://en.wikipedia.org/wiki/Dancing_Links and
https://en.wikipedia.org/wiki/Algorithm_X for the algorithm.
"""
def __init__(self, constraint_count, matrix):
matrix.sort()
headers = [AlgorithmXNode() for _ in range(constraint_count)]
for row, cols in matrix:
first = None # first node in row
for col in cols:
node = AlgorithmXNode(row)
# Insert in column
node.insert_above(headers[col])
# Insert in row
if first is None:
first = node
else:
node.insert_after(first)
# Header row
self.root = AlgorithmXNode()
last = self.root
for header in headers:
header.insert_after(last)
last = header
self.solution = []
def solve(self):
if self.root.right == self.root:
# All constraints have been satisfied
return True
# Find column with least number of nodes
header = min(self.root.loop('right'), key=attrgetter('value'))
if header.value == 0:
# No valid solution exists
return False
header.cover()
for row in header.loop('down'):
for node in row.loop('right'):
node.cover()
if self.solve():
# Add row to solution
self.solution.append(row.value)
return True
# Try a different value
for node in row.loop('left'):
node.uncover()
header.uncover()
# Backtrack
return False