项目地址:https://github.com/John-zjm/suduku
GUI地址:https://github.com/John-zjm/sudoku_Gui
需求分析
1.生成终局 格式:sudoku.exe -c n
1)不重复
2)1<=n<=1000000,能处理非法参数
4)n在1000以内时,要求程序在 60 s 内给出结果
5)输出生成的数独终盘至sudoku.txt
2. 求解数独 格式:sudoku.exe -s path
1)从指定文件读取,每读81个数字视作一道数独题,忽略其他字符
2)要求文件内的数独题目是合法的
3)文件内数独个数在1000以内时,要求程序在 60 s 内给出结果
4)输出已读入数独的答案至sudoku.txt。若存在未满81个的数字,在已解出的答案后输出“存在错误格式!”
在写界面的时,只要稍作修改就可以使用后两部分。
详细描述一下这三个部分:
输入略过,有Python的异常处理很好写
生成数独终局用的是行列变换法。数独中间的九宫格经过行列变换可以变换为2!×3!×3!×2!×3!×3!=5184(因为固定左上角为5)。这样我需要生成1000000/5184=192个九宫格就够了,而正中九宫格有8!=40320个足够满足要求。
解数独我主要参考了《数独求解的候选数优化算法设计》这篇论文,运用了显性候选数规则、隐性候选数规则、九宫格交叉排除规则。Python的numpy可以很好的对数组进行计算。在进行dfs时,我对候选数进行了估值,具体是(10-候选个数) + 同行确定数字个数 + 同列确实数字个数
sudoku.exe -c 20
sudoku.exe -c 1000000
sudoku.exe -s C:\Users\0\OneDrive\1.txt
sudoku.exe -s C:\
sudoku.exe -p asd
“错误数独”
sudo=[[1,2,3,4,5,6,7,8,9]for row in range(9)]
sudo=[[0 for col in range(9)]for row in range(9)]
sudo =[[8,0,0, 0,0,0, 0,0,0]
[0,0,3, 6,0,0, 0,0,0]
[0,7,0, 0,9,0, 2,0,0]
[0,5,0, 0,0,7, 0,0,0]
[0,0,0, 0,4,5, 7,0,0]
[0,0,0, 1,0,0, 0,3,0]
[0,0,1, 0,0,0, 0,6,8]
[0,0,8, 5,0,0, 0,1,0]
[0,9,0, 0,0,0, 4,0,0]]
由图可以看出combine()和check_one_possible()消耗较大
def combine(self, c1, c2, c3, r1, r2, r3):
self.table = deepcopy(self.temp)
if (c1 == 1):
self.colExchange(1, 2)
if (c2 == 1):
self.colExchange(4, 5)
if (c2 == 2):
self.colExchange(3, 4)
if (c2 == 3):
self.colExchange(3, 4)
self.colExchange(4, 5)
if (c2 == 4):
self.colExchange(3, 5)
self.colExchange(4, 5)
if (c2 == 5):
self.colExchange(3, 5)
if (c3 == 1):
self.colExchange(7, 8)
if (c3 == 2):
self.colExchange(6, 7)
if (c3 == 3):
self.colExchange(6, 7)
self.colExchange(7, 8)
if (c3 == 4):
self.colExchange(6, 8)
self.colExchange(7, 8)
if (c3 == 5):
self.colExchange(6, 8)
if (r1 == 1):
self.rowExchange(1, 2)
if (r2 == 1):
self.rowExchange(4, 5)
if (r2 == 2):
self.rowExchange(3, 4)
if (r2 == 3):
self.rowExchange(3, 4)
self.rowExchange(4, 5)
if (r2 == 4):
self.rowExchange(3, 5)
self.rowExchange(4, 5)
if (r2 == 5):
self.rowExchange(3, 5)
if (r3 == 1):
self.rowExchange(7, 8)
if (r3 == 2):
self.rowExchange(6, 7)
if (r3 == 3):
self.rowExchange(6, 7)
self.rowExchange(7, 8)
if (r3 == 4):
self.rowExchange(6, 8)
self.rowExchange(7, 8)
if (r3 == 5):
self.rowExchange(6, 8)
self.sudoku_map.append(deepcopy(self.table))
# 显性候选数
def _check_one_possbile(self):
# 同一行只有一个数字的情况
for r in range(0, 9):
values = filter(lambda x: isinstance(x, list), self.value[r])
for c, item in enumerate(self.value[r]):
if isinstance(item, list):
for value in item:
if sum(map(lambda x: x.count(value), values)) == 1:
self.value[r, c] = value
self.new_points.put((r, c))
return True
# 同一列只有一个数字的情况
for c in range(0, 9):
values = filter(lambda x: isinstance(x, list), self.value[:, c])
for r, item in enumerate(self.value[:, c]):
if isinstance(item, list):
for value in item:
if sum(map(lambda x: x.count(value), values)) == 1:
self.value[r, c] = value
self.new_points.put((r, c))
return True
# 九宫格内的单元格只有一个数字的情况
for r, c in self.base_points:
values = filter(lambda x: isinstance(x, list),
self.value[r:r+3, c:c+3].reshape(1, -1)[0])
for m_r, row in enumerate(self.value[r:r+3, c:c+3]):
for m_c, item in enumerate(row):
if isinstance(item, list):
for value in item:
if sum(map(lambda x: x.count(value), values)) == 1:
self.value[r+m_r, c+m_c] = value
self.new_points.put((r+m_r, c+m_c))
return True
# 找到下一个全排列
i = len(sudo_num)-2
while i >= 0 and sudo_num[i] >= sudo_num[i+1]:
i -= 1
j = i + 1
k = len(sudo_num) - 1
while sudo_num[i] >= sudo_num[k]:
k -= 1
(sudo_num[i], sudo_num[k]) = (sudo_num[k], sudo_num[i])
sudo_num[j:] = sudo_num[:j-1:-1]
这部分属于生成数独,主要对九宫格进行变换,生成下一个非递增的全排列,以实现变换中心九宫格。
# 九宫格交叉排除规则
def _check_same_num(self):
for b_r, b_c in self.base_points:
block = self.value[b_r:b_r+3, b_c:b_c+3]
# 判断数字1~9在该九宫格的分布情况
data = block.reshape(1, -1)[0]
for i in range(1, 10):
result = map(lambda x: 0 if not isinstance(
x[1], list) else x[0] + 1 if x[1].count(i) else 0, enumerate(data))
result = filter(lambda x: x > 0, result)
r_count = len(result)
if r_count in [2, 3]:
# 2或3个元素才有可能同一行或同一列
rows = map(lambda x: (x-1) / 3, result)
cols = map(lambda x: (x-1) % 3, result)
if len(set(rows)) == 1:
# 同一行,去掉其他行的数字
result = map(lambda x: b_c + (x-1) % 3, result)
row = b_r + rows[0]
for col in range(0, 9):
if col not in result:
item = self.value[row, col]
if isinstance(item, list):
if item.count(i):
item.remove(i)
# 判断移除后,是否剩下一个元素
if len(item) == 1:
self.new_points.put((row, col))
self.value[row, col] = item[0]
return True
elif len(set(cols)) == 1:
# 同一列
result = map(lambda x: b_r + (x-1)/3, result)
col = b_c + cols[0]
for row in range(0, 9):
if row not in result:
item = self.value[row, col]
if isinstance(item, list):
if item.count(i):
item.remove(i)
# 判断移除后,是否剩下一个元素
if len(item) == 1:
self.new_points.put((row, col))
self.value[row, col] = item[0]
return True
这部分属于解决数独。为九宫格交叉排除规则。(九宫格交叉排除规则) 若同一个九 宫格里,某个数字 x 仅出现在格子 d1, d2,…, dn (2 ≤n ≤ 3) 的候选数集合 ω1, ω2,…, ωn 中,且 d1, d2,…, dn 是在同一行(或列),那么同一行(或列) 的其他未填入数字格子d1, d2,…, dm(1 ≤ m ≤9 - n) 候选数集合 ω’1, ω’ 2,…, ω’m可以将数字x排除。
如图
在图中,第二个九宫格中的B行出现了2,9两次。则B8可以排除2,B3可以排除9。
while True:
self.dig_hole()
if (self.hoels > 30):
if (self.lev > level*30): # 猜测次数0~30位简单,30~60为中等,60~无穷为困难
break
if self.hoels > 30+level*10: # 30~40空为简单 40~50为中等 50~60为困难
break
这部分属于生成数独游戏,根据解数独的猜测次数来判断,如果猜测次数小于30或空为40个,则为简单。以此类推。
def check_value(self, row, col):
b_r = int(row/3)*3
b_c = int(col/3)*3
# 行
for i in range(1, 10):
sum = 0
record = -1
for j in range(9):
if self.map[row][j] == i:
sum = sum + 1
if sum == 1:
record = j # 有一个
elif sum == 2: # 有两个重复数字
if self.isyuan[row][record] != 1: # 是否为题目数字
self.heng_base[row][record] = 1
self.show_num[row][record].setStyleSheet(
"background-color: red;")
record = -1
if self.isyuan[row][j] != 1:
self.heng_base[row][j] = 1
self.show_num[row][j].setStyleSheet(
"background-color: red;")
elif sum > 2: # 有更多
if self.isyuan[row][j] != 1:
self.heng_base[row][j] = 1
self.show_num[row][j].setStyleSheet(
"background-color: red;")
if (record != -1): # 无重复
self.heng_base[row][record] = 0
# 在其他方面没有错误
if (self.shu_base[row][record] == 0) & (self.kuai_base[row][record] == 0):
if self.isyuan[row][record] != 1:
self.show_num[row][record].setStyleSheet(
"background-color: green;")
# 列
for i in range(1, 10):
sum = 0
record = -1
for j in range(9):
if self.map[j][col] == i:
sum = sum + 1
if sum == 1:
record = j
elif sum == 2:
if self.isyuan[record][col] != 1:
self.shu_base[record][col] = 1
self.show_num[record][col].setStyleSheet(
"background-color: red;")
record = -1
if self.isyuan[j][col] != 1:
self.shu_base[j][col] = 1
self.show_num[j][col].setStyleSheet(
"background-color: red;")
elif sum > 2:
if self.isyuan[j][col] != 1:
self.shu_base[j][col] = 1
self.show_num[j][col].setStyleSheet(
"background-color: red;")
if (record != -1):
self.shu_base[record][col] = 0
if (self.heng_base[record][col] == 0) & (self.kuai_base[record][col] == 0):
if self.isyuan[record][col] != 1:
self.show_num[record][col].setStyleSheet(
"background-color: green;")
# 九宫格
for i in range(1, 10):
sum = 0
record = [-1, -1]
for jrow in range(3):
for jcol in range(3):
if self.map[jrow+b_r][jcol+b_c] == i:
sum = sum + 1
if sum == 1:
record = [jrow+b_r, jcol+b_c]
elif sum == 2:
if self.isyuan[record[0]][record[1]] != 1:
self.kuai_base[record[0]][record[1]] = 1
self.show_num[record[0]][record[1]].setStyleSheet(
"background-color: red;")
record = [-1, -1]
if self.isyuan[jrow+b_r][jcol + b_c] != 1:
self.kuai_base[jrow+b_r][jcol + b_c] = 1
self.show_num[jrow+b_r][jcol +
b_c].setStyleSheet("background-color: red;")
elif sum > 2:
if self.isyuan[jrow+b_r][jcol + b_c] != 1:
self.kuai_base[jrow + b_r][jcol + b_c] = 1
self.show_num[jrow+b_r][jcol +
b_c].setStyleSheet("background-color: red;")
if (record[0] != -1):
self.kuai_base[record[0]][record[1]] = 0
if(self.heng_base[record[0]][record[1]] == 0) & (self.shu_base[record[0]][record[1]] == 0):
if self.isyuan[record[0]][record[1]] != 1:
self.show_num[record[0]][record[1]].setStyleSheet(
"background-color: green;")
这部分为界面部分。主要为动态的显示填入数字的对错。如果是对的则是绿色,如果是错的,则是红色。
这次的项目,第一部分完成的较快,没有出现太大的问题。但在附加题部分,我因为对qt的信号不熟练,导致出了很多Bug,耽误了很多时间。总体来说,这个项目使我的python有了一个提高。