基于猜想与排除法并存的解数独方法
基础9x9数独的规则:
1.每行9个数字中必须出现1~9九个数字
2.每列9个数字中必须出现1~9九个数字
3.每个九宫格中必须出现1~9九个数字
共27个条件。
我们先观察以下数独,可以发现
+-----+-----+-----+
|5 | | |
| 9| 7 6| 2 |
| 2 |5 8| 4|
+-----+-----+-----+
| 3 |8 |7 2|
|8 | 3 |6 9 |
| 2| | |
+-----+-----+-----+
| 9 |4 1| |
| 7 | 5 | 6 |
| 6 | |2 5|
+-----+-----+-----+
其中的一部分九宫格中没有的数字可以通过其他格中已有的数字的纵横排除来确定,一部分九宫格中,多个格子的可能性类似为(1,2,4),(2,4),(2,4)的形式,在九宫格中则可以确定第一个格子为1,将这些方法称为“直接确定”。这种确定方法保证在数独前提下一定正确,同理,如果按照该方法推导发现自相矛盾,则该数独原题一定无解。
+-----+-----+-----+
[5] | | 6|
| 9| 7[6]<5>2 | 直接确定
| 2 [5] 8| 4|
+-----+-----+-----+
| 3 |8 |7 2|
|8 | 3 [6]9 1|
| 2<6> | | 直接确定
+-----+-----+-----+
|2 9 5|4[6]1| |
| 7 8| 5 | 6 |
| 6 | 8 |2 [5]
+-----+-----+-----+
将这两种方法同理扩展到整个数独中即可大幅提高完整度,减少每次猜测的成本,结合猜测树,即可在短短几次猜想内完成数独。
先实现一个简单的数独类
class Sudoku:
def __init__(self, sudoku_str=None):
if sudoku_str:
self.board = [[int(n) for n in ln] for ln in sudoku_str.split(',')]
else:
self.board = [[0] * 9 for _ in range(9)]
现在实现直接确定方法
def get_row(self, row):
'''
获取一行的所有数字存在数
'''
pattern = [0] * 9
for c in self.board[row]:
pattern[c - 1] += bool(c)
return pattern
def get_col(self, col):
'''
获取一列的所有数字存在数
'''
pattern = [0] * 9
for c in [row[col] for row in self.board]:
pattern[c - 1] += bool(c)
return pattern
def get_zod(self, **kwargs):
'''
可以用不同方法获取某一宫的所有数字存在数
'''
if 'col' in kwargs:
col = kwargs['col'] // 3 * 3
row = kwargs['row'] // 3 * 3
else:
col, row = [x * 3 for x in divmod(kwargs['zod'], 3)]
pattern = [0] * 9
for i in range(row, row + 3):
for j in range(col, col + 3):
pattern[self.board[i][j] - 1] += bool(self.board[i][j])
return pattern
def get_prob(self, row, col):
'''
返回一个坐标的所有可能
'''
if self.get(row, col):
return [self.get(row, col)]
pat_col = self.get_col(col)
pat_row = self.get_row(row)
pat_zod = self.get_zod(col=col, row=row)
pattern = [pat_col[x] + pat_row[x] + pat_zod[x] for x in range(9)]
prob = []
for i in range(9):
if pattern[i] == 0:
prob.append(i + 1)
return prob
def domain_row(self, row):
'''
返回某行中可以百分百确定的方格及其值
'''
result = {}
probs = {}
count = [[] for _ in range(9)]
for col in range(9):
if self.get(row, col) == 0:
probs[(row, col)] = set(self.get_prob(row, col))
[[count[num - 1].append(coords) for num in nums] for coords, nums in probs.items()]
for col in range(9):
if len(count[col]) == 1:
result[count[col][0]] = col + 1
return result
def domain_col(self, col):
'''
返回某列中可以百分百确定的方格及其值
'''
result = dict()
probs = {}
count = [[] for _ in range(9)]
for row in range(9):
if self.get(row, col) == 0:
probs[(row, col)] = set(self.get_prob(row, col))
[[count[num - 1].append(coords) for num in nums] for coords, nums in probs.items()]
for row in range(9):
if len(count[row]) == 1:
result[count[row][0]] = row + 1
return result
def domain_zod(self, **kwargs):
'''
返回某宫中可以百分百确定的方格及其值
'''
result = {}
probs = {}
count = [[] for _ in range(9)]
if 'row' in kwargs:
zod_row = kwargs['row'] // 3 * 3
zod_col = kwargs['col'] // 3 * 3
elif 'zodrow' in kwargs:
zod_row = kwargs['zodrow'] * 3
zod_col = kwargs['zodcol'] * 3
else:
zod_col, zod_row = [x * 3 for x in divmod(kwargs['zod'], 3)]
for row in range(zod_row, zod_row + 3):
for col in range(zod_col, zod_col + 3):
if self.get(row, col) == 0:
probs[(row, col)] = set(self.get_prob(row, col))
[[count[num - 1].append(coords) for num in nums] for coords, nums in probs.items()]
for zod in range(9):
if len(count[zod]) == 1:
result[count[zod][0]] = zod + 1
return result
以上类方法大同小异,代码重复率很高,也有一些无用操作,望大佬指导简化
现在实现对整个数独进行填充的方法
def _do_domain(self):
'''
对27个条件进行一次排除操作
'''
domain = dict()
for x in range(9):
domain.update(self.domain_row(x))
domain.update(self.domain_row(x))
domain.update(self.domain_zod(zod=x))
if not domain:
return False
for coords, value in domain.items():
self.set(*coords, value)
return True
def _do_prob(self):
'''
对27个条件进行一次唯一可能值检查操作(这个比较快)
'''
ret = False
for row in range(9):
for col in range(9):
if self.get(row, col) == 0:
probs = self.get_prob(row, col)
if len(probs) == 1:
self.set(row, col, probs[0])
ret = True
return ret
def is_safe(self):
'''
为保证验证时安全,用于时刻检查所有空位是否均有候选值
'''
for row in range(9):
for col in range(9):
if len(self.get_prob(row, col)) == 0:
return False
return True
def is_valid(self):
'''
为保证验证时安全,用于时刻检查27个条件是否满足
'''
for q in range(9):
for t in self.get_col(q):
if t >= 2:
return False
for t in self.get_row(q):
if t >= 2:
return False
for t in self.get_zod(zod=q):
if t >= 2:
return False
return True
def proving(self):
'''
对数独进行初步填充
由于填充后可能满足再次填充条件,故采取多次填充直到不再有变化
'''
continue_condition = True
while continue_condition:
continue_condition = any([self._do_prob(), self._do_domain()])
if not self.is_valid():
return False
if not self.is_safe():
return False
return True
现在我们已经能得到文章开头的第二幅数独了,之后的推算公式长而复杂,使用猜测并再次填充是一个很好的方法
为了对接猜测树类,我们对类初始化方法进行一些小改造:
class Sudoku:
def __init__(self, sudoku_str=None, super_sudoku=None):
if sudoku_str:
self.board = [[int(n) for n in ln] for ln in sudoku_str.split(',')]
elif super_sudoku:
self.board = super_sudoku
else:
self.board = [[0] * 9 for _ in range(9)]
同时我们再补充一些实用方法:
def copy(self):
'''
获取一个数独实例副本
'''
return Sudoku(super_sudoku=deepcopy(self.board))
def get_empty_cell(self):
'''
从头开始寻找一个可能性个数最少的空格,之后将用于猜测树的起始点
'''
for max_prob in range(2, 10):
for row in range(9):
for col in range(9):
if 1 < len(self.get_prob(row, col)) <= max_prob:
return row, col
return -1, -1
现在我们希望拥有一个树,用于自动生成下一步的操作
可以通过递归来实现:
SOLVED = 'SOLVED'
ANTINOMY = 'ANTINOMY'
FULL_ANTINOMY = 'FULL_ANTINOMY'
class SudokuNode:
def __init__(self, sudoku):
self.sudoku = sudoku
# self.uuid = random.randint(100,999)
def solve(self):
'''
如果自身数独完整/矛盾则返回SOLVED/ANTINOMY
否则开始创建子节点
当子节点返回SOLVED时
亲节点也返回SOLVED并将自己的sudoku设置为子节点的sudoku
当子节点返回ANTINOMY或FULL_ANTINOMY时
亲节点创建新的子节点并根据新子节点的值决策
亲节点无法再创建子节点时,返回FULL_ANTINOMY
最终,返回值是SOLVED时,该实例下的sudoku便是数独的解
返回值是ANTINOMY或FULL_ANTINOMY时,该数独无解。
'''
# print(f'{self.uuid}:数独开始解决')
self.sudoku.proving()
self.guess_row, self.guess_col = self.sudoku.get_empty_cell()
if self.guess_row == -1:
if self.sudoku.is_valid and self.sudoku.is_safe():
# print(f'{self.uuid}:数独已解决')
return SOLVED
else:
# print(f'{self.uuid}:数独自相矛盾')
return ANTINOMY
self.probs = self.sudoku.get_prob(self.guess_row, self.guess_col)
for child_sudoku_node in self.summon_child_sudoku():
child_solve = child_sudoku_node.solve()
if child_solve == SOLVED:
self.sudoku = child_sudoku_node.sudoku
# print(f'{self.uuid}:子数独已解决,传递自子数独{child_sudoku_node.uuid}')
return SOLVED
elif child_solve == ANTINOMY:
# print(f'{self.uuid}:子数独自相矛盾,传递自子数独{child_sudoku_node.uuid}')
continue
elif child_solve == FULL_ANTINOMY:
# print(f'{self.uuid}:子数独尝试用尽,传递自子数独{child_sudoku_node.uuid}')
continue
# print(f'{self.uuid}:数独猜测树枝穷')
return FULL_ANTINOMY
def summon_child_sudoku(self):
'''
通过solve()已获取的参数下生成子数独。
注意此处不修改原数独,而是修改拷贝后的数独并赋值给实例
'''
for i in range(len(self.probs)):
child_sudoku = self.sudoku.copy()
child_sudoku.set(self.guess_row, self.guess_col, self.probs[i])
# print(f'{self.uuid}:生成了一个子数独')
yield SudokuNode(child_sudoku)
(此处可以自行导random包后去掉日志注释,然后查看节点的传递细节)
令主节点执行SudokuNode.solve()
后,数独的结果会从子数独一步步传递到主节点SudokuNode.sudoku
上,SudokuNode.solve()
的返回值如果为SOLVED
就说明原数独有解,可以通过查看节点的sudoku来确定解。
最后,才疏学浅,由于该代码首要用途为展示,虽然考虑了可读性但不多(注释极少),同时一些地方为可拓展性和思路整理做的比较冗余(如矛盾和路穷其实没必要分离讨论,solve方法里连那四句elif条件都根本不用写),故效率还有很高提升空间,希望各位大佬批评指正。本代码从思路到成品均为独立思考得到,因而其中的英语水平不高(比如九宫格,我第一时间想到的是zodiac,意识到很离谱的时候已经写了一半了,但因为和row,col看起来很搭就没做大改),本代码跑几个例题数独基本可以做到0.5s以内解决,比较刁钻的全空数独也可以做到0.7s左右,但大数据集还没做过实验,望诸位前辈多多指教
最后放一下完整的源程序代码,包含显示用的__str__
和调试用的highlight_str
等方法:
import time
from copy import deepcopy
SOLVED = 'SOLVED'
ANTINOMY = 'ANTINOMY'
FULL_ANTINOMY = 'FULL_ANTINOMY'
class Sudoku:
def __init__(self, sudoku_str=None, super_sudoku=None):
if sudoku_str:
self.board = [[int(n) for n in ln] for ln in sudoku_str.split(',')]
elif super_sudoku:
self.board = super_sudoku
else:
self.board = [[0] * 9 for _ in range(9)]
def copy(self):
return Sudoku(super_sudoku=deepcopy(self.board))
def get(self, row, col):
return self.board[row][col]
def set(self, row, col, value):
self.board[row][col] = value
return value
def get_row(self, row):
pattern = [0] * 9
for c in self.board[row]:
pattern[c - 1] += bool(c)
return pattern
def get_col(self, col):
pattern = [0] * 9
for c in [row[col] for row in self.board]:
pattern[c - 1] += bool(c)
return pattern
def get_zod(self, **kwargs):
if 'col' in kwargs:
col = kwargs['col'] // 3 * 3
row = kwargs['row'] // 3 * 3
else:
col, row = [x * 3 for x in divmod(kwargs['zod'], 3)]
pattern = [0] * 9
for i in range(row, row + 3):
for j in range(col, col + 3):
pattern[self.board[i][j] - 1] += bool(self.board[i][j])
return pattern
def domain_row(self, row):
result = {}
probs = {}
count = [[] for _ in range(9)]
for col in range(9):
if self.get(row, col) == 0:
probs[(row, col)] = set(self.get_prob(row, col))
[[count[num - 1].append(coords) for num in nums] for coords, nums in probs.items()]
for col in range(9):
if len(count[col]) == 1:
result[count[col][0]] = col + 1
return result
def domain_col(self, col):
result = dict()
probs = {}
count = [[] for _ in range(9)]
for row in range(9):
if self.get(row, col) == 0:
probs[(row, col)] = set(self.get_prob(row, col))
[[count[num - 1].append(coords) for num in nums] for coords, nums in probs.items()]
for row in range(9):
if len(count[row]) == 1:
result[count[row][0]] = row + 1
return result
def domain_zod(self, **kwargs):
result = {}
probs = {}
count = [[] for _ in range(9)]
if 'row' in kwargs:
zod_row = kwargs['row'] // 3 * 3
zod_col = kwargs['col'] // 3 * 3
elif 'zodrow' in kwargs:
zod_row = kwargs['zodrow'] * 3
zod_col = kwargs['zodcol'] * 3
else:
zod_col, zod_row = [x * 3 for x in divmod(kwargs['zod'], 3)]
for row in range(zod_row, zod_row + 3):
for col in range(zod_col, zod_col + 3):
if self.get(row, col) == 0:
probs[(row, col)] = set(self.get_prob(row, col))
[[count[num - 1].append(coords) for num in nums] for coords, nums in probs.items()]
for zod in range(9):
if len(count[zod]) == 1:
result[count[zod][0]] = zod + 1
return result
def _do_domain(self):
domain = dict()
for x in range(9):
domain.update(self.domain_row(x))
domain.update(self.domain_row(x))
domain.update(self.domain_zod(zod=x))
if not domain:
return False
for coords, value in domain.items():
self.set(*coords, value)
return True
def _do_prob(self):
ret = False
for row in range(9):
for col in range(9):
if self.get(row, col) == 0:
probs = self.get_prob(row, col)
if len(probs) == 1:
self.set(row, col, probs[0])
ret = True
return ret
def proving(self):
continue_condition = True
while continue_condition:
continue_condition = any([self._do_prob(), self._do_domain()])
if not self.is_valid():
return False
if not self.is_safe():
return False
return True
def is_safe(self):
for row in range(9):
for col in range(9):
if len(self.get_prob(row, col)) == 0:
return False
return True
def is_valid(self):
for q in range(9):
for t in self.get_col(q):
if t >= 2:
return False
for t in self.get_row(q):
if t >= 2:
return False
for t in self.get_zod(zod=q):
if t >= 2:
return False
return True
def get_prob(self, row, col):
if self.get(row, col):
return [self.get(row, col)]
pat_col = self.get_col(col)
pat_row = self.get_row(row)
pat_zod = self.get_zod(col=col, row=row)
pattern = [pat_col[x] + pat_row[x] + pat_zod[x] for x in range(9)]
prob = []
for i in range(9):
if pattern[i] == 0:
prob.append(i + 1)
return prob
def __str__(self):
split_line = '+-----+-----+-----+\n'
ret_str = ''
for i in range(9):
if i % 3 == 0:
ret_str += split_line
ln = list('|' + ' '.join([' 123456789'[self.board[i][j]] for j in range(9)]) + '|\n')
ln[6] = '|'
ln[12] = '|'
ret_str += ''.join(ln)
ret_str += split_line
return ret_str
def highlight_str(self, row, col):
split_line = '+-----+-----+-----+\n'
ret_str = ''
for i in range(9):
if i % 3 == 0:
ret_str += split_line
ln = list('|' + ' '.join([' 123456789'[self.board[i][j]] for j in range(9)]) + '|\n')
ln[6] = '|'
ln[12] = '|'
if i == row:
ln[2 * col] = '>'
ln[2 * col + 2] = '<'
ret_str += ''.join(ln)
ret_str += split_line[:-1]
return ret_str
def get_empty_cell(self):
for max_prob in range(2, 10):
for row in range(9):
for col in range(9):
if 1 < len(self.get_prob(row, col)) <= max_prob:
return row, col
return -1, -1
class SudokuNode:
def __init__(self, sudoku):
self.sudoku = sudoku
# self.uuid = uuid.uuid4()
def solve(self):
# print(f'{self.uuid}:数独开始解决')
self.sudoku.proving()
self.guess_row, self.guess_col = self.sudoku.get_empty_cell()
if self.guess_row == -1:
if self.sudoku.is_valid and self.sudoku.is_safe():
# print(f'{self.uuid}:数独已解决')
return SOLVED
else:
# print(f'{self.uuid}:数独自相矛盾')
return ANTINOMY
self.probs = self.sudoku.get_prob(self.guess_row, self.guess_col)
for child_sudoku_node in self.summon_child_sudoku():
child_solve = child_sudoku_node.solve()
if child_solve == SOLVED:
self.sudoku = child_sudoku_node.sudoku
# print(f'{self.uuid}:子数独已解决,传递自子数独{child_sudoku_node.uuid}')
return SOLVED
elif child_solve == ANTINOMY:
# print(f'{self.uuid}:子数独自相矛盾,传递自子数独{child_sudoku_node.uuid}')
continue
elif child_solve == FULL_ANTINOMY:
# print(f'{self.uuid}:子数独尝试用尽,传递自子数独{child_sudoku_node.uuid}')
continue
# print(f'{self.uuid}:数独猜测树枝穷')
return FULL_ANTINOMY
def summon_child_sudoku(self):
for i in range(len(self.probs)):
child_sudoku = self.sudoku.copy()
child_sudoku.set(self.guess_row, self.guess_col, self.probs[i])
# print(f'{self.uuid}:生成了一个子数独')
yield SudokuNode(child_sudoku)
另有使用例献上,欢迎核查运行结果并检验
sudoku_s1 = ','.join(['500000000',
'009076020',
'020508004',
'030800702',
'800030690',
'002000000',
'090401000',
'070050060',
'060000205'])
sudoku_s2 = ','.join(['100409007',
'000000000',
'703006100',
'000032060',
'090001270',
'000008040',
'400000010',
'800090054',
'610000080', ])
sudoku_s3 = ','.join(['0'*9]*9)
s1 = Sudoku(sudoku_s1)
print(s1)
sn1 = SudokuNode(s1)
st = time.time()
print(sn1.solve())
et = time.time()
print(sn1.sudoku)
print(f'用时:{et - st:.6}s')
s2 = Sudoku(sudoku_s2)
print(s2)
sn2 = SudokuNode(s2)
st = time.time()
print(sn2.solve())
et = time.time()
print(sn2.sudoku)
print(f'用时:{et - st:.6}s')
s3 = Sudoku(sudoku_s3)
print(s3)
sn3 = SudokuNode(s3)
st = time.time()
print(sn3.solve())
et = time.time()
print(sn3.sudoku)
print(f'用时:{et - st:.6}s')