23.10.1假期无聊,玩了几个数独游戏,发现难度系数越高,解题速度越慢,最后cpu发烫了也没有解出最难的那一关,临近收假,我们考虑用代码来完成这一烧脑的过程,并总结下玩数独的解题思路。
一、数独的规则和解题思路
-
数独上的每一个格子,以其为中心的横向和竖向为1-9的不重复数字组成,并在每一个9方格中也是此规律。
-
对于简单的数独,通常利用排除法,每次会确定一到多个符合要求的唯一解的格子,对于复杂的高难度的数独,通常需要从候选的排列组合中先假设一个确定值,在后续的逻辑推算中,如果假设值满足要求则表明假设正确,不满足则排除此假设值。
-
然而对于高阶的数独,往往需要假定了很多的值,到推算时发现出现矛盾时,你也不知道哪一步假定值错误,因为这样的排列组合太多了,导致你的逻辑推算错误的假定值不一定为你假定的最后一个值,他可能出现在你假定中的任何一个值中。
二、初级数独的代码求解
1.给数独的每一个格子占位,并输入已知的、给出的数字,未填入的格子数值为0(自定义占位符)
import random
def orgin():
global num2
global input1
num2 = []
for a in range(1,10):
num1 = []
for b in range(1,10):
num1.append(0)
num2.append(num1)
input1 = [(1,2,3),(1,3,7),(1,4,1),(1,8,6),(2,3,4),(2,5,5),(2,8,3),(3,7,4),(4,1,9),(4,2,8),(4,4,3),(5,1,6),(5,4,7),(5,6,2),(5,9,9),(6,6,6),(6,8,5),(6,9,2),(7,3,1),(8,2,9),(8,5,3),(8,7,7),(9,2,7),(9,6,9),(9,8,8),(9,7,5)]
for a in input1:
num2[a[0]-1][a[1]-1]=a[2]
orgin()
其中input1变量中储存的已知格子的数字,如(1,2,3)表示第一行第二列的数字为3.运行orgin()后我们将得到如下排列的数独
[0, 3, 7, 1, 0, 0, 0, 6, 0]
[0, 0, 4, 0, 5, 0, 0, 3, 0]
[0, 0, 0, 0, 0, 0, 4, 0, 0]
[9, 8, 0, 3, 0, 0, 0, 0, 0]
[6, 0, 0, 7, 0, 2, 0, 0, 9]
[0, 0, 0, 0, 0, 6, 0, 5, 2]
[0, 0, 1, 0, 0, 0, 0, 0, 0]
[0, 9, 0, 0, 3, 0, 7, 0, 0]
[0, 7, 0, 0, 0, 9, 5, 8, 0]
2.满足上述数独规则的情况下,获取每个格子可能的数字,如果出现矛盾值,则返回num22 = []
def combine():
global num22
global com2
num22 = num2
com2 = []
for a in [2,5,8]:
com4 = []
for b in [2,5,8]:
com3 = []
for c in [-1,0,1]:
for d in [-1,0,1]:
com3.append((num2[a+c-1][b+d-1]))
com4.append(com3)
com2.append(com4)
for x in range(1,10):
for y in range(1,10):
if num22[x-1][y-1] == 0:
if (x <=3) & (x >= 1) & (y <=3) & (y >= 1):
jiu = com2[0][0]
elif (x <=3) & (x >= 1) & (y <=6) & (y >= 4):
jiu = com2[0][1]
elif (x <=3) & (x >= 1) & (y <=9) & (y >= 7):
jiu = com2[0][2]
elif (x <=6) & (x >= 4) & (y <=3) & (y >= 1):
jiu = com2[1][0]
elif (x <=6) & (x >= 4) & (y <=6) & (y >= 4):
jiu = com2[1][1]
elif (x <=6) & (x >= 4) & (y <=9) & (y >= 7):
jiu = com2[1][2]
elif (x <=9) & (x >= 7) & (y <=3) & (y >= 1):
jiu = com2[2][0]
elif (x <=9) & (x >= 7) & (y <=6) & (y >= 4):
jiu = com2[2][1]
elif (x <=9) & (x >= 7) & (y <=9) & (y >= 7):
jiu = com2[2][2]
num3 = [m[y-1] for m in num2]
num4 = []
for z in range(1,10):
if (z not in (num22[x-1]+num3)) & (z not in jiu):
num4.append(z)
if num4 == []:
num22 = []
return num22
break
else:
num22[x-1][y-1]=num4
return num22
3.变量com1记录拥有唯一数字的每个格子的位置和值,其中x,y表示位置坐标,x表示行,y表示列,num22[x-1][y-1]表示该位置坐标的值。
com1 = []
def xunh():
global com1
if num22 != []:
for x in range(1,10):
for y in range(1,10):
if (type(num22[x-1][y-1]) != int):
if (len(num22[x-1][y-1]) == 1):
com1.append((x,y,num22[x-1][y-1]))
return com1
4.运行上述函数后,将获得的唯一值的格子填充进数独中,并利用all()函数对新生成的数独进行更新,并记录新的唯一值的格子,并结果一并更新的com1变量中,直至没有出现含有唯一值的格子。
orgin()
combine()
xunh()
def all():
global i
global num2
global num22
i = 0
while i < 81:
i += 1
orgin()
for a in com1:
num2[a[0]-1][a[1]-1]= a[2][0]
combine()
xunh()
all()
5.利用com1记录的唯一值的格子填充原始的数独,即可获得初级数独的答案
orgin()
for a in com1:
num2[a[0]-1][a[1]-1]= a[2][0]
答案如下:
[0, 3, 7, 1, 0, 0, 0, 6, 0]
[0, 0, 4, 0, 5, 0, 0, 3, 0]
[0, 0, 9, 0, 0, 0, 4, 0, 0]
[9, 8, 2, 3, 0, 0, 0, 0, 0]
[6, 0, 5, 7, 0, 2, 0, 0, 9]
[0, 0, 3, 0, 0, 6, 0, 5, 2]
[0, 0, 1, 0, 0, 0, 0, 0, 0]
[0, 9, 8, 0, 3, 0, 7, 0, 0]
[0, 7, 6, 0, 0, 9, 5, 8, 0]
com1变量为[(6, 3, [3]), (5, 3, [5]), (4, 3, [2]), (9, 3, [6]), (8, 3, [8]), (3, 3, [9])],如(6,3,[3])表示第六行第三列的格子拥有唯一值3;现在只找到6个含有唯一值的格子,并没有求出所有的值。这是因为目前展示的是等级为非常困难的数独,如果是中低等级的数独是可以全部求出答案的,有兴趣的可以尝试一下。
三、 高级数独解法
1.在初级数独的解法基础上继续;我们将含有两个可能值的格子找出,并利用mm变量储存初级算中单一值的格子。
mm = len(com1)
com5 = []
for x in range(1,10):
for y in range(1,10):
if (type(num22[x-1][y-1]) != int):
if (len(num22[x-1][y-1]) == 2):
com5.append((x,y,num22[x-1][y-1]))
com5变量为:[(4, 3, [6, 8]), (4, 5, [2, 4]), (4, 7, [4, 8]), (5, 2, [1, 4]), (5, 5, [2, 4]), (6, 1, [2, 5]), (6, 3, [1, 6]), (6, 7, [4, 7]), (7, 1, [5, 8]), (7, 2, [5, 7]), (7, 8, [3, 8]), (7, 9, [3, 7]), (9, 1, [8, 9]), (9, 3, [1, 8]), (9, 8, [2, 8])],如(4,3,[6,3])表示第六行第三列的格子可能为6也可能为3。
2.将全部的含有两个可能值的格子进行随机抽样,并将抽样的结果加入com1中,抽样的结果在com1中满足横坐标或者纵坐标相同时不同格子的值必须不同,如果相同的则重新抽样,直至满足要求。然后运行all()函数,如果num22 = [],或者新生成com1中不满足逻辑,则表明生成的新的com1对于后续单一值格子的逻辑推算有矛盾的地方,则com1的值回到抽样前,进行重新抽样,直至满足要求。
num22 = []
while (aa != 0) or num22 == []:
aa = 1
com1 = com1[0:mm]
while (aa != 0):
com1 = com1[0:mm]
aa = 0
for e in com5:
com1.append((e[0],e[1],[random.choice(e[2])]))
for a in com1:
for b in com1:
if (a[0] == b[0]):
if (a != b):
if a[2] == b[2]:
aa += 1
if (a[1] == b[1]):
if (a != b):
if a[2] == b[2]:
aa += 1
all()
for a in com1:
for b in com1:
if (a[0] == b[0]):
if (a != b):
if a[2] == b[2]:
aa += 1
if (a[1] == b[1]):
if (a != b):
if a[2] == b[2]:
aa += 1
3.运行orgin(),将num2归位到初始的数独,利用com1储存的单一值格子进行填充,并获得求出全解的数独
orgin()
for a in com1:
num2[a[0]-1][a[1]-1]= a[2][0]
4.结果如下:
[8, 3, 7, 1, 9, 4, 2, 6, 5]
[1, 6, 4, 2, 5, 7, 9, 3, 8]
[2, 5, 9, 8, 6, 3, 4, 1, 7]
[9, 8, 2, 3, 4, 5, 1, 7, 6]
[6, 1, 5, 7, 8, 2, 3, 4, 9]
[7, 4, 3, 9, 1, 6, 8, 5, 2]
[4, 2, 1, 5, 7, 8, 6, 9, 3]
[5, 9, 8, 6, 3, 1, 7, 2, 4]
[3, 7, 6, 4, 2, 9, 5, 8, 1]
大家可以看看,结果是否符合要求。
四、代码数独游戏算法总结
-
矢量性:需要给每个格子占位,因为每个格子值与横行和竖行以及9宫格的值相关(1-9不重复),所以体现出位置属性、方向属性,也叫矢量
-
降维:无论是初级数独还是高级数独,最后都需要转化为定向和定量,也就是说不管哪个格子,必须要确定符合要求的唯一值,也就是上述我们用random.choice()函数进行取值的原因
-
发展的眼光:计算机语言中变量是可以被赋值,因此做好复位和多次判断是很重要的,因为新变量可以悄无声息的覆盖旧变量的,这很符合马克思主义思想:用发展的眼光看待事物的发展;在这里就是用更新的思路看新的变量