您的位置:首页 > 编程语言 > Python开发

数独终局生成与求解python实现

2018-11-28 21:37 465 查看

Github项目地址

Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
计划 30 20
估计在这个任务需要多少时间 10 15
需求分析(包括学习新技术) 60 120
生成设计文档 200 100
设计复审
代码规范 60 60
具体设计 120 200
具体编码 2000 2500
代码复审
测试 60 180
报告 60 100
测试报告 30 20
计算工作量 30 30
事后总结 30 30
合计 2690 3375

终局生成

思路

第一行9个数,除去第一个数要求固定为8外,其余的8个数有8!= 40,320种排列
而对应每种排列情况,可以把排列依次旋转0,3,6,1,4,7,2,5,8个数,生成40320个终局,而对此终局还可以,交换1~3行内可以交换两行(即这3行可以任意顺序排列),4~6行、7~9行亦是如此,同理列方向也可如此。
由此,我们可以得到8!・3!・3!=1,451,520已经大于一百万的要求
又因为第一个数要求固定,我们只需要任意改变4~6行、7~9行的排列顺序即可

相关部分代码如下:

# 对第一行不同的排列,生成不同的局面
def create_grid(self, row):
grid = []
for slice_x in self.order:
grid.append(row[-slice_x:]+row[:-slice_x])
return grid

# 生成第一行的全排列
def permutation(self, a_row):
if not a_row:
self.perm.append(list(self.tmp_row))
return
for i in a_row:
self.tmp_row[self.cur] = i
self.cur += 1
tmp_ls = list(a_row)
tmp_ls.remove(i)
self.permutation(tmp_ls)
self.cur -= 1

函数的调用关系图如下:

解数独

算法的基本思路

采用回溯法,逐行逐列的进行回溯直到全部求解出来或者判断无解

函数的调用关系如下:

相关部分代码如下

def solve(self, row_n, col_n):
# 如果当前列超出总列数则进入下一行第一列
if col_n == 9:
row_n += 1
col_n = 0
# 直到找到一个空格
while True:
# 若遍历完仍没有空,说明已完成填空,返回
if row_n > 8:
return True
if self.mark[row_n][col_n]:
break
col_n += 1
if col_n == 9:
row_n += 1
col_n = 0
while True:
self.a_plz[row_n][col_n] = self.find_next(row_n, col_n, self.a_plz[row_n][col_n] + 1)
if self.a_plz[row_n][col_n] == 0:
break
self.rule(row_n, col_n, False)
tmp_flag = self.solve(row_n, col_n+1)
if tmp_flag:
return True
self.rule(row_n, col_n, True)
return False

性能分析与改进

按照上述思路,把代码实现后发现性能远不如想象中的好

用性能分析工具进行分析,结果如下:

生成终局的性能分析


从上面分析可以看出有很大一部分时间花费在了文件读写上,于是,就想到通过一块一块的读写来代替现在的逐字符读写,改进之后,果然性能好了很多

解数独的性能分析


消耗最大的是求解数独的主要函数

后来发现,同学用C写的和我用的同样的算法,各方面性能都远远的超过我的。
于是想到用cython进行优化,把频繁调用的函数用cython改写

单元测试

由于用cython优化后,文件内大多函数对外不可见,无法进行单元测试。
故本单元测试是在cython优化之前进行的,即先通过单元测试确保了程序的正确性之后,再用cython进行优化

针对每个单元设计多个测试用例(总用例数大于10个),确保能覆盖大部分情况

对输入进行测试

给出参数个数过多,过少,格式错误的用例

对类初构造函数测试

构造多个不同的类的实例,检测是否被正确初始化

对生成终局函数进行测试

测试输出到文件中的结果是否符合数独规则,
以及检查生成的终局是否有重复

对解数独函数进行测试

对比数独题目文件和解文件中的结果是否相符并且正确
测试当文件中题目无效时,输出是否和预期相符

对类中的负责处理各种情况的main函数测试

对函数的各分支分别进行测试
对比输出结果是否与预期相符

下面是测试代码覆盖率

其中test.py是测试代码,因为其中一些语句只有在测试未通过时才会执行,所以当全部通过时这部分测试代码未被覆盖到

界面

点击Next按钮生成下了题目,点击OK按钮检查当前的填的解的正确性,分别会有相应的弹窗提醒

相关部分代码如下

def gui(self):
root = tk.Tk()
# 设置窗口宽度与高度不可变
root.resizable(False, False)
root.title("Sudoku")

# 从文件中读取并解码生成临时图标,用完后立马删除
tmp = open("tmp.ico", "wb+")
tmp.write(base64.b64decode(ICO_IMG))
tmp.close()
#im = Image.open("tmp.ico")
#img = ImageTk.PhotoImage(im)
#root.tk.call('wm', 'iconphoto', root._w, img)
root.iconbitmap('tmp.ico')
os.remove("tmp.ico")
tmp = open("tmp.jpg", "wb+")
tmp.write(base64.b64decode(JPG_IMG))
tmp.close()
tmp_image = Image.open("tmp.jpg")
photo = ImageTk.PhotoImage(tmp_image)
label = tk.Label(root, image=photo)
os.remove("tmp.jpg")
# for i in range(11):
#     root.rowconfigure(i,weight=1)
#     root.columnconfigure(i,weight=1)
# root.rowconfigure(11,weight=1)

# 生成空格
for i in range(11):
for j in range(11):
if i != 3 and i != 7 and j != 3 and j != 7:
self.value[i][j] = tk.StringVar()
self.ety[i][j] = tk.Entry(root, textvariable=self.value[i][j], width=2, font=90)
self.ety[i][j].grid(row=i, column=j, padx=12, pady=12, sticky='NSEW')

# 生成第一个题目并显示
self.bind()

label.grid(row=0, column=0, rowspan=12, columnspan=11, sticky='NSEW')

# 确定按钮
submit_btn = tk.Button(root, text='OK', command=lambda:self.check())
submit_btn.grid(row=11, column=9, pady=10, ipadx=30, columnspan=2)

# next按钮
next_btn = tk.Button(root, text='Next>', command=lambda:self.bind())
next_btn.grid(row=11, column=0, pady=10, ipadx=20, columnspan=2)

root.mainloop()
阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: