-
Notifications
You must be signed in to change notification settings - Fork 0
/
SudokuSolver.py
240 lines (214 loc) · 9.7 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
240
from copy import deepcopy
from settings import ENABLE_DEBUG
class SudokuSolver(object):
""" SudokuSolver class. This class requires a list representation
of a sudoku, and will then try to solve it.
Usage:
Instantiate an object from the class and pass a start_board.
Then you can use the exposed methods.
Exposed methods:
solve -- Attempts to solve the sudoku
board_is_valid -- Check wheter the board with which the
object is instantiated is a valid board. """
def __init__(self, start_board):
''' Initializer for the SodukoSolver object.
Requires a sudoku board as argument.
Assumes the given board is validated.'''
self.is_solved = False
self.board = deepcopy(start_board)
def solve(self, board):
''' Main sudoku solver routine. Note: this method works
recursively. It's a brute-force approach (very) loosely based on
https://en.wikipedia.org/wiki/Sudoku_solving_algorithms '''
# Pointer to keep track of the next position to be
# checked. Represented as next_pos[row, col].
next_pos = [0, 0]
# If there is no empty new location available, all
# squares are filled in and the sudoku is considered complete.
if not self.next_empty_location(board, next_pos):
self.board = board
self.is_solved = True
return True
# Update the current row and col to check based on the pointer.
# Remember: the pointers were updated to represent a new square.
current_row = next_pos[0]
current_col = next_pos[1]
# Go over digits 1 up to and including 9.
# Note: start value is inclusive, end value is exclusive
for value in range(1, 10):
if self.is_legal_move(board, current_row,
current_col, value):
# Check if the considered move is legal
# If it is legal, assign value to the square
board[current_row][current_col] = value
# Do all of the above recursively until we found a
# solution or considered every possible value. Remember:
# the next_pos gets updated each time. Which makes this
# brute-force algorithm.
if self.solve(board):
return True
# If we tried all options (1 to 9) recursively and encountered
# an error, reset the tried square to zero and try again.
board[current_row][current_col] = 0
# By returning false we "drop down a level" in the recursive trace.
# This way, we can backtrack through the trace without having to keep
# track of a regular stack/queue
return False
def board_is_valid(self):
''' Check if a given board solely exists of legal positions.
A position is considered legal if and only if:
- The positions value is unique in its row AND
- The positions value is unique in its column AND
- The positions value is unique in its 3x3 box
A board is considered valid if all of it's inputted
values comply with the above three rules. '''
if ENABLE_DEBUG:
print("GLOBAL DEBUG -- Checking to see if starting board is"
" valid.")
board = self.board
is_valid = True # Innocent until proven otherwise
for i, row in enumerate(board):
for j, col in enumerate(row):
# Temporarily unset given position, because the checked value
# will always be set in its own row, column and box.
_, board[i][j] = self.board[i][j], None
# Squares containing a value of zero are unsolved and can thus
# be skipped. Since all unsolved squares are represented by 0,
# they will never be unique in the row, column and box.
if _ != 0:
is_valid = self.is_legal_move(board, i, j, _)
board[i][j] = _
if not is_valid:
return is_valid
board[i][j] = _
return is_valid
def exists_in_column(self, board, col, val):
''' Determine if a given value exists in the
given column. '''
for i in range(9):
if board[i][col] == val:
return True
return False
def exists_in_row(self, board, row, val):
''' Determine if a given value exists in the
given row. '''
for i in range(9):
if board[row][i] == val:
return True
return False
def exists_in_box(self, board, row, col, val):
''' Determine if a given value exists in the
given 3x3 box. '''
for i in range(3):
for j in range(3):
# Given row/col is not always top left of box
# so this needs to be taken into account
if board[i+(row - row % 3)][j+(col - col % 3)] == val:
return True
return False
def is_legal_move(self, board, row, col, val):
''' Takes a given row, column and value and decides wheter
placing the value at this row/column is permitted or not.
A move is considered legal if:
- The value is unique to its own row
- The value is unique to its own column
- The value is unique to its own 3x3 box
- The value is a value in the inclusive set of [1,9]'''
if (not self.exists_in_column(board, col, val) and not
self.exists_in_row(board, row, val) and not
self.exists_in_box(board, row, col, val) and
1 <= val <= 9):
return True
return False
def next_empty_location(self, board, next_pos):
''' Determines the next empty location that is available
on the board. Next_pos is a list passed in, and contains
pointers (represented as [row, col]) to the next available
position. If a proper new location is found, the next_pos
pointer gets updated and True is returned. If there is no
empty position, False is returned. '''
for row in range(9):
for col in range(9):
if board[row][col] == 0:
# A value of 0 is regarded as an empty square
# If we encounter it, we found the next empty
# square. Which means we can update the next_pos
# pointer.
next_pos[0] = row
next_pos[1] = col
return True
return False
def print_sudoku(self):
''' Prints a given sudoku grid to console. '''
hor_line = "++---+---+---++---+---+---++---+---+---++"
print(hor_line.replace('-', '='))
for i, row in enumerate(self.board):
cur_line = ""
for j, val in enumerate(row):
cur_line += "|"
if j % 3 == 0:
cur_line += "|"
if val is 0:
cur_line += ' . '
else:
cur_line += ' {} '.format(val)
cur_line += "||"
print(cur_line)
if (i+1) % 3 == 0:
print(hor_line)
class SudokuGrid(object):
def __init__(self, puzzle_list):
''' Initializer for a new SudokuGrid object.
This class can create an empty sudoku grid,
print a sudoku grid to console and has a method
to manually fill a sudoku grid with values for testing
purposes. '''
# Create empty board
self.board = self.create_board()
self.board = self.fill_puzzle_start(self.board, puzzle_list)
def create_board(self):
''' Creates an empty sudoku grid. '''
num_rows = 9
num_cols = 9
empty_grid = [
[0 for cols in range(num_cols)]
for rows in range(num_rows)
]
return empty_grid
def fill_puzzle_start(self, empty_grid, puzzle_list):
''' Allows developers to fill the sudoku grid with manual
values for testing purposes. '''
# Fill the grid with manual values for testing purposes
puzzle_start = copy(empty_grid)
# First row
# puzzle_start = [
# [5, 3, 0, 0, 7, 0, 0, 0, 0],
# [6, 0, 0, 1, 9, 5, 0, 0, 0],
# [0, 9, 8, 0, 0, 0, 0, 6, 0],
# [8, 0, 0, 0, 6, 0, 0, 0, 3],
# [4, 0, 0, 8, 0, 3, 0, 0, 1],
# [7, 0, 0, 0, 2, 0, 0, 0, 6],
# [0, 6, 0, 0, 0, 0, 2, 8, 0],
# [0, 0, 0, 4, 1, 9, 0, 0, 5],
# [0, 0, 0, 0, 8, 0, 0, 7, 9]
# ]
puzzle_start = puzzle_list
return puzzle_start
def print_puzzle_fancy(self, board):
''' Prints a given sudoku grid to console. '''
hor_line = "++---+---+---++---+---+---++---+---+---++"
print(hor_line.replace('-', '='))
for i, row in enumerate(board):
cur_line = ""
for j, val in enumerate(row):
cur_line += "|"
if j % 3 == 0:
cur_line += "|"
if val is 0:
cur_line += ' . '
else:
cur_line += ' {} '.format(val)
cur_line += "||"
print(cur_line)
if (i+1) % 3 == 0:
print(hor_line)