第二个求解案例,我想到了数独。曾经有一段时间对数独求解非常感兴趣,在学习C++语言时也编写了数独求解程序。本次Python学习,要求比之前高一点,把出题程序也一起编写了吧。
1、数独简介
数独 (英语:Sudoku)是一种逻辑性的数字填充游戏,玩家须以数字填进每一格,而每行、每列和每个宫(即3x3的大格)有1至9所有数字。游戏设计者会提供一部分的数字,使谜题只有一个答案。答案满足同一个数字不可能在同一行、列或宫中出现多于一次。
2、算法
2.1 数独解题算法
最简单直接的算法就是:从第一个空格开始,尝试填入数字;之后再尝试下一个空格;如果失败,就回退,成功就继续,直至所有空格填满。具体如下:
(1)使用9X9二维矩阵表示数独局面,0表示空格,非零表示已经填写数字。从第一个元素开始,转步骤(2)
(2)搜索下一个空格。
(2-1)搜索到空格,转步骤(3);
(2-2)搜索不到空格,说明所有空格填写完毕,搜索到一个成功解题方案,结束程序。
(3)按照数独规则搜索当前空格可填写的数字集合。
(3-1)搜索不到可填写数字。当前方案不可行,将当前格恢复为0,回退一步,重新填写当前格数字。
(3-2)搜索到可填写的数字集合。从集合中取出一个数字,填入当前格。转步骤(2)。
第(2)、(3)步可以用递归程序实现,比较好实现回退和继续搜索功能。
这个算法简单粗暴,求解一般的数独题目都没有问题。但是在后续出题程序中,用这个算法测试出题结果是否可行时,经过大量随机数据测试,发现以下问题:
(1)效率低。有时甚至程序长时间运行不出结果,只能强行中止。
(2)一次只能搜索一个解法,不能测试题目的求解结果是否唯一。
分析问题(1)的原因主要在于初始搜索的可选数字较多,造成程序需要搜索方案数目剧增。因此,可以计算当前局面中所有空格的可选数字数目,从最小可选数字数目空格开始尝试填写数字。这样可以大大减少需要搜索方案数目。同时,为了避免频繁计算所有空格的可选数字数目,将搜索到的空格的可选数字集合存起来,在设置空格新数字、回退时更新相关空格的可选数字集合。
问题(2)的原因在于搜索到一个方案就结束,可以设置搜索的可行方案数目上限(上限设置为2就可以搜索求解方案是否唯一),达不到上限就回退继续搜索。
修改后的算法如下:
(1)使用9X9二维矩阵表示数独局面,0表示空格,非零表示已经填写数字。设置求解方案数目上限为NUP,搜索当前局面所有空格的可选数字集合,存入二维数组lsava中。转步骤(2)
(2)根据lsava,搜索可选数字数目最小的空格。
(2-1)搜索到空格,转步骤(3);
(2-2)搜索不到空格,说明所有空格填写完毕。搜索到一个成功解题方案,存储当前成功方案至成功方案列表。如果成功方案列表总数等于NUP,结束程序;否则将当前格恢复为0,恢复相关空格可选数字集合至lsava,回退一步,重新填写当前格数字。
(3)从lsava中取得当前空格可填写的数字集合。
(3-1)数字集合为空。当前方案不可行,将当前格恢复为0,恢复相关空格可选数字集合至lsava。回退一步,重新填写当前格数字。
(3-2)搜索到可填写的数字集合。从集合中取出一个数字,填入当前格。更新相关空格可选数字集合至lsava,同时暂存更新之前的集合,用于后续可能的回退操作。转步骤(2)。
2.2 数独出题算法
出题方法一:
最先想到的出题方法是仿照解题过程,从空盘开始逐渐填写数字:
- 从全部为空的局面开始;
- 在所余空格中随机挑选一个;
- 在选定空格的可用数字集合中随机挑选一个数字填入空格;
- 利用数独解题算法判断当前局面是否有解,如果否,则回退一步,恢复当前空格,转到步骤(3);如果有解,进一步判断解是否唯一,如果唯一,则找到结果,返回,否则转步骤(2)。
该算法的实现详见3.2。运行后发现,尽管采用了2.1中的优化后求解策略,但是由于随机设置数字过程很容易出现很多不可解局面,耗费大量搜索时间,有时甚至无法在10分钟内求解出结果。
为了解决这个问题,查阅网络文献,有了出题方法二:
主要思路是从一个完成的数独结果开始,随机挖除一定数目的空格,然后再判断当前局面解是否唯一。这样就避免的无解情况,搜索效率大幅提升。
挖除的空格数目越多,题目求解难度越高,同时多解概率也越高。可以设置空格数目的上限和下限值,调节所出题目的难度。
方法2需要大量数独结果,本算法使用一个固定数独结果,通过随机交换1-9数字位置产生所需大量数独结果,理论上可以产生9!= 362880个结果。再考虑题目上随机位置挖洞,可以产生的数独题目足够用了。具体如下:
- 通过随机交换1-9数字位置,产生一个数独结果;
- 设置空格数目的上限和下限值;
- 随机产生上限空格个位置,挖空位置,并保存挖空前的数值。
- 从空格上限开始,向下限方向搜索;
- 填入一个挖空前的数字。判断当前局面结果是否唯一,如果唯一,则结束;否则继续步骤(5)。
- 如果搜索不到唯一解,转置步骤(1)尝试下一个数独结果。
3、算法实现
3.1 数独解题
主程序Shudu1.py,输入输出及程序调用。
'''' 数独搜索 读取用户输入文件,搜索结果。如果得不到结果,则提示失败。 ''' import sys import CShudu1 as csd # 主程序 if len(sys.argv)<2 : print("使用方法: ") print(sys.argv[0],"FileName") print("FileName为输入文件文件名称前缀,真正输入文件为 Filename.dat, 输出文件名称为FileName.res。") exit(1) fin=sys.argv[1]+".dat" fout=sys.argv[1]+".res" print("输入文件:",fin,"输出文件",fout) #初始化 cs=csd.CShudu() nret=cs.ReadData(fin) if nret!=0: print("读取输入文件错误!") exit(2) print("原始数据:") cs.PrintData() # 开始查找 cs.CalLsava() nr=cs.getNumRes(1) if len(cs.lsres)>=1 : print("成功找到{}个结果!".format(len(cs.lsres))) cs.loadres(0) cs.PrintData() nret=cs.WriteData(fout) if nret!=0: print("写输出文件错误!") exit(3) else : print("本题无解!")
|
子程序CShudu1.py,以类的形式封装数独解题算法。
import random ''' 封装为类的数独搜索 ''' class CShudu : def __init__(self): # 初始化 # 数据 self.clear()
def clear(self): # 清除数据 #当前局面数据 self.data=[[0 for i in range(9)] for j in range(9)] #当前局面每个格子的可用数据列表 self.lsava=[[set() for i in range(9)] for j in range(9)] #找到的可行解列表 self.lsres=[] # 从保持data数据至lsres def saveres(self): self.lsres.append([self.data[i].copy() for i in range(9)]) #print("in saveres",self.lsres) # 从lsres装载数据至data def loadres(self,k): #print("in loadres",self.lsres) self.data=[self.lsres[k][i].copy() for i in range(9)] # 从文件读矩阵数据 def ReadData(self,fname) : try: f=open(fname,"r") except FileNotFoundError: #打开文件错误 return 1 for i in range(9): str=f.readline() lstr=str.split() #文件读取错误 if len(lstr) < 9 : return 2 self.data[i]=[int(lstr[j]) for j in range(9)] f.close() |