Python(+numpy)实现对9*9数独问题(单解或多解)的快速递归求解

Python(+numpy)实现对9*9数独问题的求解

利用Python(+numpy库)递归实现对9*9数独问题的求解

(=== 分享一下这两天断断续续写的解9*9数独问题的经历及源码,第一次写博客,很多功能不太会用,也会有很多不到位的地方,谢大家指正!===)

# 整活

百度 wd=世界最难数独
Python(+numpy)实现对9*9数独问题(单解或多解)的快速递归求解_第1张图片
输入方式及运行结果,运行时间(完全遍历结束,时间戳分别在递归函数前后)为0.88秒的亚子(膨胀)Python(+numpy)实现对9*9数独问题(单解或多解)的快速递归求解_第2张图片

# 以下正文

回顾一下 数独(Sudoku)

Python(+numpy)实现对9*9数独问题(单解或多解)的快速递归求解_第3张图片
无论是4*4还是9*9的数独游戏规则很简单很粗暴,拿9*9数独来说,规则可概括为 9组 1-9 共 81个数字(包含已给出的数字)填入 9*9 的方格内,满足条件每行,每列,每个九宫格都有一组(1-9)的数字,从解的角度也可描述为每行,每列,每九宫格不存在重复的数字

着手数独问题的解决

首先游戏规则简介明了,对写代码实现部分功能而言是一件好事

废话少说!以下按(之前已经写好的代码中)函数模块顺序分析

1. 数独题目导入

要解决实际的数独问题,首先要将带有已知量的数独题目导入函数中,(笔者懂得太少只能选择手打) 手动输入不失为一个最通用的途径,将题目视作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

2. 已知内容的初始化

由上步我们已经得到包含解题需要的全部信息的列表S,但是这样的一维列表是不方便我们下一步操作的,所以我们引入numpy库函数,将列表S进一步转为9*9的二维数组S,这样的二维数组才是符合我们思考习惯的,也便于之后对S的增删查改操作。

  • 定义待填区(S中0值元素区)类变量(blank类)及初始化方法
    包含该待填区三元坐标(行r,列c,宫区b 均为0至8的整数)
    该待填区取值范围 (1-9的整数)由其三元坐标及已知数据确定
    程序的最终目的即待填区的定值
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  # 该空白区取值范围

  • 定义二维数组S初始化函数
    此函数将遍历数组S中的数据,目的是将已知数组S的已知区(非0项) 数据放入精确到各(行,列,宫)的二层列表(R,C,B)中存放并返回,即 可直接通过(行,列,宫)的三元坐标读取任一行,列,宫中已有的数字,以便于下一步待填区初始化函数的调取
    S_orignal函数代码块(S为9*9 np.array型数组):
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
  • 定义待填区初始化函数
    此函数同样会遍历二维数组S中数据,找出所有待填区(0项),并以之前定义的,包含待填区三元坐标及取值范围的(blank)型变量存储在 BLANK列表 中,并按取值范围由小到大的顺序排序后返回,其中待填区的取值范围将通过上一步函数返回的列表(R,C,B)确定
    Blank_orignal函数代码块(S为9*9 np.array型数组):
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
  • 定义检查函数
    定义检查函数的目的,第一个作用是对最开始手动导入的列表S进行检查(主要检查各行,列,宫(R,C,B列表)是否有重复数字),判断所给初始列表是否符合规则(这里有个问题想问问大家,当初始列表,即所给题目符合规则,数独问题是否存在无解情况,我主观想着应该都是有解的(大脑:我累了)),第二个作用是下一步递归时,检查选值填空时是否会出现让部分待填区出现无值可填的情况(主要检查BLANK列表中元素是否出现取值范围列表长度为0的情况)
    check函数代码块(返回判断值):
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

3. 递归求解(核心)

此步是求解数独问题的关键!
首先,当题目符合规则,在解题的任何阶段,对任意待填区b,总存在取值唯一亦或可取多个值的情况,由blank取值范围(blank.rangerlist)的长度可快速判断。因为BLANK列表已排序,直接判断其第一个元素取值范围是否==1即可。

定义递归函数start(S)

该函数将用于递归,是整个程序的核心。

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库多次测试下,可以评论或私信我(大声)

你可能感兴趣的:(这是啥?,数独游戏)