自动求解数独(9 X 9)

在疫情防控的关键时刻,在家闲来无事,就尝试着在手机上玩数独游戏。但是解起来比较让人头疼,一不小心填错就要从头再来,费时耗力。(但是对于锻炼思维是极好的,哈哈哈哈哈哈哈哈嗝…)
为了能够快速解决数独问题,顺便温习温习一个寒假没有摸过的python和算法,所以就计划自己编程来实现数独的自动填充。

采用的思想是**回溯**。
通俗一点说就是从头到尾一个一个试,有点类似于深度优先搜索,当进行到某一个位置不能再向下进行时,就退回一步继续这个过程。
当从头到尾遍历完成,也就是当深度搜索一直到达了最后一个节点,所填入的数据均合乎规则,则此时填入的这一系列数字组合就是原始数独的解,此时即完成了数独的求解。

算法描述如下:

算法:
输入:数独阵。9行9列,没有数字的填0。
输出:填写完整的数独阵。
步骤:
1.	根据输入阵input_array,计算得到标志阵flag, 这是一个二值阵,1表示有原始数据,0表示没有。
2.	根据输入阵input_array,计算得到每一个未填写数据的位置处,不允许再填入的数据,用一个三维列表存贮,记为invalid_init。
3.	再根据flag阵,计算得到每一个0位置处对应的回溯位置坐标值,用于后面回溯,得到loc阵。
4.	从(0,0)开始循环遍历输入阵:
5.	While  i < 9:
6.	     J = 0
7.	     While  j < 9:
8.	                If  flag[I,j] == 1:
9.	                    继续下一个点,j++
10.	                Else:
11.	                   找出(i,j)位置处允许填入的有效数字
12.	                   循环遍历有效数字:
13.	                       填入一个数,判断是否合法
14.	                           若合法,则跳出循环,j++,继续下一个位置
15.	                           否则,将当前填入的值加入该位置对应的非法值列表中。
16.	                   循环结束后判断循环结束的原因,
17.	                      若是遍历完了所有有效数字后结束,说明该位置已经无法填值
18.	                          进行回溯
19.	                          回溯时将该位置的非法值列表还原到最初
20.	     I++
21.	循环结束后,即可完成数独填写。

具体代码实现如下(采用的语言为python):

import numpy as np
import copy

# 得到一个二维numpy阵,元素为0或者1,0代表原始输入无数据,1代表原始输入已填入数字
def getflag_array(input_array):
    return np.int64(input_array > 0)

# 得到输入矩阵每一个位置上在回溯时应该跳转的位置,类似于父节点的位置
def getloclist(flag):
    m,n = np.shape(flag)
    N = [[[] for _ in range(n)] for _ in range(m)]      # 存储回溯时的路径
    temp = [-1,-1]
    flg = True
    for i in range(m):
        for j in range(n):
            if flag[i,j] == 1:
                N[i][j] = [-1,-1]
            elif flg:
                N[i][j] = [i,j]             # 第一个0所在位置处,在回溯时还是自身位置
                temp = [i,j]
                flg = False
            else:
                N[i][j] = temp            # 其余均为上一个0出现的位置
                temp = [i,j]
    return N


# 得到矩阵每一个未填的位置处对应的 ★★★不可以填入★★★ 的数字。
def get_invalid_mat(input_array):
    m,n = np.shape(input_array)                          # m行n列
    M = [[[] for _ in range(n)] for _ in range(m)]
    for i in range(m):
        for j in range(n):
            if input_array[i,j] == 0:         # 原始输入没有数据
                row = input_array[i,:].flatten().tolist()        # 得到行列及3X3方块内的数据并转成list类型
                column = input_array[:,j].flatten().tolist()
                square = input_array[3*(i//3):3*(i//3)+3,3*(j//3):3*(j//3)+3].flatten().tolist()
                getunion = set(row+column+square) - set([0])    # 得到并集,把零去掉
                M[i][j] = list(getunion)                        # 赋值
    return M


def solve(input_array,num):
    total = set([i for i in range(1,10)])      # 总共允许填入的数字集合 1~9
    flag = getflag_array(input_array)          # 标志阵
    invalid_init = get_invalid_mat(input_array)      # 非法值阵
    invalid = copy.deepcopy(invalid_init)          # 深拷贝,  ★★★★关键一步★★★
    loc = getloclist(flag)                     # 回溯路径
    i = 0                                  # 初始化
    while(i<num):
        j = 0
        while(j<num):
            if flag[i,j] == 1:            # 若原始有数据,则直接跳过
                j += 1
            else:
                if input_array[i,j] != 0:    # 这一步是为了保证在回溯时,将回溯的节点的值也设为非法,避免死循环
                    invalid[i][j].append(input_array[i,j])
                valid = list(total - set(invalid[i][j]))
                k = 0
                length = len(valid)
                while(k < length):                 # 依次判断每一个合法值
                    input_array[i,j] = valid[k]    # 尝试填入原始阵中
                    if judge(input_array,i,j):          # 判断填入后是否符合规则,符合则返回true
                        j += 1                          # 继续判断下一个点
                        break                          # 跳出当前循环
                    else:
                        invalid[i][j].append(valid[k])    # 否则,将当前的值也设置为无效
                        k += 1                            # 继续判断下一个有效值
                if k >= length:                # 说明该位置所有数据都不行,则需要返回到上一步的位置处,进行回溯
                    input_array[i,j] = 0       # 原始矩阵中该位置数据置零
                    invalid[i][j] = invalid_init[i][j].copy()    # 将该位置的无效值还原到最初状态,★★★重要★★★
                    i,j = loc[i][j]                       # 根据回溯路径,跳到上一个位置处
        i += 1
    return input_array


# 判断i、j位置处所在的行、列、方块是否符合规则
def judge(input_array,i,j):
    row = input_array[i,:].flatten()                  # 得到行列方块数值,并拉平,得到的是array
    column = input_array[:,j].flatten()
    square = input_array[3*(i//3):3*(i//3)+3,3*(j//3):3*(j//3)+3].flatten()
    row_ = row[row > 0].tolist()                      # 取出非零值并转成list
    column_ = column[column > 0].tolist()
    square_ = square[square > 0].tolist()
    return (len(row_) == len(set(row_))) and \
           (len(column_) == len(set(column_)))\
           and (len(square_) == len(set(square_)))          # 利用集合操作去重,从而判断原列表中是否有重复元素


# 主函数
def main():
    # 输入待解决的数独,空用0代替
    input_array = np.array([
                   [0,3,0,0,0,0,4,0,1],
                   [5,0,0,3,0,0,0,0,0],
                   [0,0,6,0,0,7,0,0,2],
                   [2,0,0,0,9,0,0,6,0],
                   [0,4,5,0,0,0,0,0,0],
                   [0,0,0,0,0,2,5,8,0],
                   [0,0,0,1,4,0,0,0,0],
                   [0,0,8,0,0,0,9,7,0],
                   [9,0,0,0,3,0,0,0,0]])
    result = solve(input_array,9)               # 解数独
    print(result)


if __name__ == '__main__':
    main()

使用上述代码中的输入,运行后即得到了填写完整的数独,如下所示:

[[8 3 7 9 2 6 4 5 1]
 [5 2 4 3 8 1 7 9 6]
 [1 9 6 4 5 7 8 3 2]
 [2 8 1 5 9 4 3 6 7]
 [6 4 5 8 7 3 2 1 9]
 [3 7 9 6 1 2 5 8 4]
 [7 5 3 1 4 9 6 2 8]
 [4 1 8 2 6 5 9 7 3]
 [9 6 2 7 3 8 1 4 5]]

经过检验正确!!!

最后,再提一个我再编写程序时遇到的问题,或者说是一个小细节,就是因为这个细节问题,我debug了大半天,~~~~~~~~~
这个细节就是在python中关于列表复制的问题。
大家应该都听过 深复制和潜复制 。知道使用 .copy()进行复制和直接用 = 的区别。
但是,最坑人的一点是,.copy()当复制的仅仅是一维列表时,是进行深复制,当是多维列表时,它仅仅是将最外层元素进行深复制,内层列表依旧是潜复制,大坑!!!!!!!
要想实现彻底的深度复制,就需要使用copy.deepcopy() 来实现深复制,切记啊!!!!!

具体可以参考这篇博客:python中的深浅复制问题
(注:虽然这篇博客中的配图有点问题,但是原理讲的还是挺清楚的。)

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页