Python(+numpy)实现对9*9数独问题的求解
(=== 分享一下这两天断断续续写的解9*9数独问题的经历及源码,第一次写博客,很多功能不太会用,也会有很多不到位的地方,谢大家指正!===)
百度 wd=世界最难数独
输入方式及运行结果,运行时间(完全遍历结束,时间戳分别在递归函数前后)为0.88秒的亚子(膨胀)
无论是4*4还是9*9的数独游戏规则很简单很粗暴,拿9*9数独来说,规则可概括为 9组 1-9 共 81个数字(包含已给出的数字)填入 9*9 的方格内,满足条件每行,每列,每个九宫格都有一组(1-9)的数字,从解的角度也可描述为每行,每列,每九宫格不存在重复的数字
首先游戏规则简介明了,对写代码实现部分功能而言是一件好事
废话少说!以下按(之前已经写好的代码中)函数模块顺序分析
要解决实际的数独问题,首先要将带有已知量的数独题目导入函数中,(笔者懂得太少只能选择手打) 手动输入不失为一个最通用的途径,将题目视作9*9方阵,按行依次输入,每9个数字为一行,其中待填空白格(blank)输入数字0。
目的是将输入的 81 个数字将转为一个 81 整型元素的列表S,至此导入工作便完成了。
(导入的操作每个人习惯不同,也会有不同的实现方法,这里只讲目的,不赘述方法)
scanf函数代码块:
def scanf():
S = []
for i in range(1,10):
while 1:
R = input('第{0}行'.format(i))
R = list(R)
if len(R) != 9:
print('Error Input')
else:
break
S = S+R
for i in S:
d = S.index(i)
S[d] = int(i)
print('=== Orignal ===\n',np.array(S).reshape(9,9))
return S
由上步我们已经得到包含解题需要的全部信息的列表S,但是这样的一维列表是不方便我们下一步操作的,所以我们引入numpy库函数,将列表S进一步转为9*9的二维数组S,这样的二维数组才是符合我们思考习惯的,也便于之后对S的增删查改操作。
class blank:
def __init__(self,row,column,block,rangelist):
self.row = row # 行(0-8)
self.column = column # 列(0-8)
self.block = block # 宫(0-8)
self.rangelist = rangelist # 该空白区取值范围
def S_orignal(S):
R = [[],[],[],[],[],[],[],[],[]]
C = [[],[],[],[],[],[],[],[],[]]
B = [[],[],[],[],[],[],[],[],[]]
for i in range(9):
for j in range(9):
if S[i,j]!= 0:
R[i].append(S[i,j])
C[j].append(S[i,j])
if i<3 and j<3:
B[0].append(S[i,j])
elif i<3 and j<6:
B[1].append(S[i,j])
elif i<3 and j>=6:
B[2].append(S[i,j])
elif i<6 and j<3:
B[3].append(S[i,j])
elif i<6 and j<6:
B[4].append(S[i,j])
elif i<6 and j>=6:
B[5].append(S[i,j])
elif i>=6 and j<3:
B[6].append(S[i,j])
elif i>=6 and j<6:
B[7].append(S[i,j])
else:
B[8].append(S[i,j])
return R,C,B
def Blank_orignal(S,R,C,B): # 获取空白区域初始化信息
BLANK = []
for i in range(9):
for j in range(9):
if S[i,j]== 0:
if i<3 and j<3:
b=0
elif i<3 and j<6:
b=1
elif i<3 and j>=6:
b=2
elif i<6 and j<3:
b=3
elif i<6 and j<6:
b=4
elif i<6 and j>=6:
b=5
elif i>=6 and j<3:
b=6
elif i>=6 and j<6:
b=7
else:
b=8
rd = [1,2,3,4,5,6,7,8,9]
for k in R[i]:
if k in rd:
rd.remove(k)
for k in C[j]:
if k in rd:
rd.remove(k)
for k in B[b]:
if k in rd:
rd.remove(k)
b0 = blank(i,j,b,rd)
BLANK.append(b0)
BLANK.sort(key = lambda l : len(l.rangelist))
return BLANK
def check(S,R,C,B,BLANK): # 检查是否有错误填充值
for i in R:
if len(set(i))!=len(i):
return 3
for i in C:
if len(set(i))!=len(i):
return 5
for i in B:
if len(set(i))!=len(i):
return 7
for i in BLANK:
if len(i.rangelist)==0:
return 9
return 1
此步是求解数独问题的关键!
首先,当题目符合规则,在解题的任何阶段,对任意待填区b,总存在取值唯一亦或可取多个值的情况,由blank取值范围(blank.rangerlist)的长度可快速判断。因为BLANK列表已排序,直接判断其第一个元素取值范围是否==1即可。
该函数将用于递归,是整个程序的核心。
1 . 引用值为S,类型为一维列表 或 np.array数组 皆可
2 . 调用数组S初始化函数 与 待定区初始化函数获得解题各阶段已知信息
3 . 通过check检查函数(不通过则结束函数 并返回0)
4 . 判断待填区列表BLANK是否为空列表(为空则意味着数组S已填满,且符合题意,此时S为原题目的一个解(为了解决多解问题路走通了就可以回头了,所以此处同样返回值为0))
5 . 此时处于解题阶段(填值阶段),(读取 b = BLANK[0])进行分支判断
存在待填区b只能取一个值(只有一条路,没必要回头):
按顺序,采用不可逆填法,直接对数组S进行修改,填一个检查一个(当有多个待填区只能取一个值时,一次填完也可以,因为过check函数时该错还是错的,错的方式不同而已,但都会被截停)返回值为下次递归时的返回值
待填区中取值范围最小的b取值范围都大于1(有多条路走,走不通要回头):
生成一个作用域只在此步的数组S1(S的复制品),存档S1当S被错误值修改或已经走通时,用于读档(回头)。从b取值范围中按顺序放入b处可能的值,直接对数组S进行修改,再次进入递归(我为什么要说再 ?),递归函数返回值即可判断是否要回头,要就读档。
注意!这是重点!:如果进入两次及以上有多条路走 或 题目有多解时,将可能出现取值范围都试被完都没有解(走不通)或已经输出过该取值时的解(已经走通过)的情况。
----总之就是这个路口的路都走完了,要回上一个路口了。
遍历完取值范围后,一定要返回0值,不仅为了函数递归调用严谨,也是对一些题目出现多解情况时,遍历所有解的基础
上代码,start(S) 递归函数:
def start(S):
R,C,B = S_orignal(np.array(S).reshape(9,9))
BLANK = Blank_orignal(np.array(S).reshape(9,9),R,C,B)
if check(S,R,C,B,BLANK) != 1:
return 0
if BLANK == []:
print('=== Solution ===')
print(np.array(S).reshape(9,9))
return 0
else:
b = BLANK[0]
if len(b.rangelist) == 1:
S[(9*b.row)+b.column] = b.rangelist[0]
return start(S)
elif len(b.rangelist) > 1:
S1 = list(S)
for j in b.rangelist:
S[(9*b.row)+b.column] = j
if start(S) == 0:
S = S1
return 0
启动语句段:
S = scanf()
R,C,B = S_orignal(np.array(S).reshape(9,9))
BLANK = Blank_orignal(np.array(S).reshape(9,9),R,C,B)
if check(S,R,C,B,BLANK) == 1:
start(S)
else:
print('=== No Solution! ===')
我看看怎么上传代码的,有兴趣玩一玩的朋友可以copy下来,在python打开文件运行,即可,按提示手打题目(实测工作量不大(双层卑微 T+T)),这两天也是想递归也是想破脑袋,也遇过递归过深等等等等的Error和bug,但最后总算还是整出来了(作者落泪)
有一说一,我觉得速度还是蛮快,我的笔记本一般的数独题目约莫零点几秒就能出第一个solution的亚子(疯狂暗示填 81 个 0 也是可以的 !),和输入时间相比微不足道(???)有兴趣的朋友可以用time库多次测试下,可以评论或私信我(大声)