高效算法求解数独


title: 高效算法求解数独
date: 2019-12-26 17:55:16
tags: 数据结构与算法
categories: 数据结构与算法


背景

  之前上python课的时候,有一次实验是求解数独,要求时间复杂度要低;为此老师讲解了一个高效的数独算法,我觉得算法挺有意思的,写篇博客记录一下。


描述

首先需要知晓数独的两个规则:

  1. 若某个位置的值已经确定,那么,和这个位置在同一行,同一列,同一个3×3的格子,都不能填写这个值,比如,九宫格(1,1)位置的值为2,那么,第一行,第一列,以及第一个3×3的格子里,都不能在填2了;
  2. 若某一行,或者某一列,或者某一个3×3里面,只有一个位置可能填1(假如是1),那么1一定是填写在这个位置,因为没有其他位置可以填它了;、

求解步骤

  1. 创建一个三维数组,假设就叫“可能值数组”,记录数独9×9的81个位置中,每个位置可能填写的值,初始情况下,每个位置的可能值都是1到9,表示每个位置都可能填写1-9中任何一个数字;

  2. 遍历数独的每一个位置,若某个位置已经有值,则将这个位置的可能值更新为这个值,比如,九宫格上,(1,1)的值已经确定是2了,那就将三维数组中(1,1)位置的可能值从[1-9]更新为[2],直到所有的位置更新完毕;

  3. 使用上述规则1进行剪枝:

    (1):从第一个位置开始遍历九宫格,若当前遍历到的位置(i,j),它的值已经知晓,那么就更新可能值数组,将第i行,第j列,以及其对应的3×3(【i/3×3 , j/3×3】就是这个3×3的第一个点)的所有位置,它们的可能值都要去除(i,j)位置的值;

    (2):若某个位置在经过上一步的剪枝后,可能值只剩下一个了,那这个位置的值就确定了,比如说,位置(1,1)的初始可能值是1到9,经过上面的一步步去除,只剩下一个3了,那这个(1,1)位置填写的值必定就是3了。此时我们可以再次使用规则1,即第一行,第一列,以及其对应的3×3中,所有的格子的可能值不能有3;

    (3):依次遍历每一个位置,使用上面的规则1,直到最后一格子,第一次剪枝便完成了;

  4. 使用上面的规则2进行剪枝:

    (1):统计每一行,每一列,以及每一个3×3中,每个数出现的次数,比如统计第一行每个格子的可能值,看1-9各出现几次,若某个可能值只出现一次,那出现这个值的位置,就是填写这个值,比如说,在第一行,3这个数字,只有(1,1)这个位置可能填写,那(1,1)就是填3,因为其他位置的可能值当中都不包含3,也就是都不能填写3;

    (2):根据上一步确定了某个位置的值后,那我们此时又可以使用规则1了,比如,上一步确定了(1,1)是填写3,那么第一行,第一列,以及第一个3×3中其余的格子, 都不能在写3了,我们从可能值数组中,将这些位置的可能,值删除3这个数;

    (3):而此时,又可能出现上面的第3步中的(3)的情况;

  5. 规则2剪枝完毕后,数独还没有解决完毕,那我们只能通过枚举每一个位置的值,来一一尝试,直到找到最后的解决办法:

    (1):我们在最开始创建了一个三维数组,存储每一个位置的可能值,初始情况下,每个位置的可能值都是1-9,但是经过上面两个规则的剪枝后,已经去除了很多;

    (2):此时我们使用DFS深度优先搜索,尝试为每一个位置填值。经过上面的剪枝,每个位置的可能值的数量应该不一样了,而为了减少DFS搜索的次数,我们应该从可能值最少的位置开始搜索;

    (3):遍历9宫格,找出还未填写值,且可能值最少的那个位置(可能有多个,找出第一个),尝试将他的第一个可能值填写在这个位置,然后再次调用规则1和规则2进行剪枝,剪枝完毕后,判断当前的九宫格中,是否有不和规则的地址,比如同一行出现两个一样的数。若没有不合法的地方,则再次进行一次(3),若有,表示这个位置不能填这个值,则从这个位置的可能值中再选择另外一个;

    (4):一直使用步骤(3),直到所有的位置都确定,则表示成功解出数独,若有某个位置,它的任何一个可能值填上去,都不能得到最终结果,那数独就是无解的;

  6. 经过上面这些步骤,就能快速的解出数独,因为主要通过规则1,2进行剪枝,大大减少了枚举的次数,提升了效率;


所需计算

  1. 已知位置(i,j),则这个位置所在的3*3,其第一个点是(i/3×3 , j/3×3),i/3×3表示先作除法,去除了小数部分,再乘3,就是3的倍数了;
  2. 已知位置(i,j),如何计算这个位置属于第几个3×3,那就是(i/3×3 + j/3),每个3*3都占3行,且3列,i/3得到这个位置在第几个3行,j/3得到这个位置在第几个3列,每三行有三个3×3,所以i/3×3 + j/3就可以得到这个位置在第几个3×3;

代码

因为是为了完成python实验,所以代码是用python写的:

# 此类用来表示搜索时,需要搜索的一个位置
# x,y为此位置的坐标,size为此位置的可能值的数量
class Node:
    def __init__(self, x, y, size):
        self.x = x
        self.y = y
        self.size = size

# 读取方式2,读取top95
def read_way2():
    # 从文件中读取初始数独
    value = [[0] * 10 for i in range(10)]
    # 读取文件2,3
    s = infile.readline();
    if s == '':
        return
    for i in range(9):
        for j in range(9):
            value[i][j] = int(s[i * 9 + j])
    return value

# 初始化函数
def init():
    value = read_way2()   # 读取top95
    # 初始化possibleValue,若当前位置有值,则其可能值就是这个值本身
    # 若没有值,则初始的可能值就是1-9
    possibleValue = [[[] for j in range(10)] for i in range(10)]
    for i in range(9):
        for j in range(9):
            if value[i][j] != 0:
                possibleValue[i][j] = [value[i][j]]
            else:
                possibleValue[i][j] = [1, 2, 3, 4, 5, 6, 7, 8, 9]

    return possibleValue

#####################################################################################################################

# 根据规则1进行剪枝
# 遍历所有的位置,找到已经确定的位置进行剪枝
def pruningByRule1(possibleValue):
    for i in range(9):
        for j in range(9):
            if len(possibleValue[i][j]) == 1:
                removeValueByRule1(i, j, possibleValue)    # 以当前位置为起点,移除重复的可能值


# 在规则1剪枝中,将同一区域中,已经确定的数移除
# 以(i,j)位置为起点,移除重复的可能值
def removeValueByRule1(i, j, possibleValue):
    # 与当前值在同一行或同一列的位置,可能值减去当前位置的值
    for k in range(9):
        # 从第i行中的可能值列表中,移除当前值
        confirmOneValueInRule1(i, k, possibleValue[i][j][0], possibleValue)
        # 从第i列中的可能值列表中,移除当前值
        confirmOneValueInRule1(k, j, possibleValue[i][j][0], possibleValue)

    # 与当前值在同3*3的位置,可能值减去当前位置的值
    for k in range(int(i / 3) * 3, int(i / 3) * 3 + 3):
        for l in range(int(j / 3) * 3, int(j / 3)* 3 + 3):
            confirmOneValueInRule1(k, l, possibleValue[i][j][0], possibleValue)

# 移除某个位置的可能值,并在移除后判断能否得到确定值
def confirmOneValueInRule1(i, j, num, possibleValue):
    if len(possibleValue[i][j]) == 1:
        return
    # 从当前位置的可能值中,移除已经确定的数
    if num in possibleValue[i][j]:
        possibleValue[i][j].remove(num)
    # 判断移除后,当前位置能否确定
    if len(possibleValue[i][j]) == 1:
        # 若当前位置确定,则以当前位置为基准进行移除操作
        removeValueByRule1(i, j, possibleValue)


###########################################################################################

# 根据规则2剪枝,判断同一个区域每个值可能出现的次数
# 若某个值可能出现的位置只有一个,表示这个值就在此位置
def pruningByRule2(possibleValue):
    # 统计第i行,数字j可能值出现了几次
    countX = [[0] * 10 for i in range(12)]
    # 统计第i列,数字j可能值出现了几次
    countY = [[0] * 10 for i in range(12)]
    # 统计第i个3*3,数字j可能值出现了几次
    countZ = [[0] * 10 for i in range(12)]

    # 统计各个区域可能值出现的次数
    for i in range(9):
        for j in range(9):
            for num in possibleValue[i][j]:
                countX[i][num] += 1
                countY[j][num] += 1
                countZ[i // 3 * 3 + j // 3][num] += 1

    # 判断哪些数字只出现了一次, 若只出现了一次的数字
    # 表示这个数字就是那个位置的答案
    for i in range(9):
        for j in range(1,10):
            # 若第i行数字j只出现了一次
            if countX[i][j] == 1:
                for k in range(9):  # 遍历第i行的每一列,判断这个唯一值出现在哪
                    confirmValueInRule2(i, k, j, possibleValue)

            # 若第i列数字j只出现了一次
            if countY[i][j] == 1:
                for k in range(9):  # 遍历第i列的每一列,判断这个唯一值出现在哪
                    confirmValueInRule2(k, i, j, possibleValue)

            # 若第i个3 * 3中,数字j的可能值只有一个
            if countZ[i][j] == 1:
                # 遍历第i个3*3的所有位置,判断这个唯一值出现在哪
                for k in range(i//3*3, i//3*3+3):
                    for l in range(i%3*3, i%3*3+3):
                        confirmValueInRule2(k, l, j, possibleValue)


# 判断当前位置是否包含某个数,包含则为此位置的答案
def confirmValueInRule2(i, j, singleNum, possibleValue):
    # 若当前位置已经确定值了, 直接返回
    if len(possibleValue[i][j]) ==1:
        return
    # 若当前位置包含唯一可能值,则这个位置的确定值就是它
    if singleNum in possibleValue[i][j]:
            possibleValue[i][j] = [singleNum]
            # 重新调用规则1
            removeValueByRule1(i, j, possibleValue)

###########################################################################################

# 递归搜索
def searchForPruning(node, possibleValue):
    # 若没有需要填值的点了,表示搜索结束,答案已出
    if node is None:
        return possibleValue

    # 获取当前位置的x,y坐标
    x = node[0]
    y = node[1]
    for num in possibleValue[x][y]:
        # 复制一份当前状态
        tempPossibleValue = copy.deepcopy(possibleValue)
        # 更新数据
        tempPossibleValue[x][y] = [num]
        # 调用规则1,2
        removeValueByRule1(x, y, tempPossibleValue)
        pruningByRule2(tempPossibleValue)

        # 调用规则1,2后,判断当前结果是否合法,若合法,则进行递归下一层
        if judge_result(tempPossibleValue):
            # 递归求解
            tempPossibleValue = searchForPruning(get_lowest_node(tempPossibleValue), tempPossibleValue)
            # 判断递归结果,若结果有返回值,则表示求解成功
            if tempPossibleValue is not None:
                return tempPossibleValue

# 获取当前可能值最小的位置
def get_lowest_node(possibleValue):
    minn = 100
    node = None
    for i in range(9):
        for j in range(9):
            # 若当前位置没有确定值,并且可能值的数量更少,则更新记录,
            if 1 < len(possibleValue[i][j]) < minn:
                minn = len(possibleValue[i][j])
                node = (i, j)
    return node


# 判断某个位置是否可以放某个值
def judge_result(possibleValue):
    # 标记某个数字是否出现
    countX = [[False] * 10 for i in range(12)]
    countY = [[False] * 10 for i in range(12)]
    countZ = [[False] * 10 for i in range(12)]

    # 统计各个区域可能值出现的次数
    for i in range(9):
        for j in range(9):
            if len(possibleValue[i][j]) == 1:
                # 若当前状态不合法,返回false
                if countX[i][possibleValue[i][j][0]] or countY[j][possibleValue[i][j][0]] or countZ[i // 3 * 3 + j // 3][possibleValue[i][j][0]]:
                    return False
                # 若合法,则标记已经确定的数字
                countX[i][possibleValue[i][j][0]] = True
                countY[j][possibleValue[i][j][0]] = True
                countZ[i // 3 * 3 + j // 3][possibleValue[i][j][0]] = True
    return True


# 判断某个位置是否可以放某个值
def judge_now_number(possibleValue, i, j, num):
    # 判断num在这一行和这一列是否被使用
    for k in range(9):
        if len(possibleValue[i][k]) == 1 and possibleValue[i][k][0] == num:
            return False
        if len(possibleValue[k][j]) == 1 and possibleValue[k][j][0] == num:
            return False
    # 判断num在这个3*3是否被使用
    for k in range(int(i / 3) * 3, int(i / 3) * 3 + 3):
        for l in range(int(j / 3) * 3, int(j / 3) * 3 + 3):
            if len(possibleValue[k][l]) == 1 and possibleValue[k][l][0] == num:
                return False
    return True

###########################################################################################


# 输出展示可能值列表
def display(possibleValue):
    for i in range(9):
        for j in range(9):
            print(possibleValue[i][j], end="---")
        print()
    print()

###########################################################################################

# 主函数
def main():
    start = time.time()
    c = 0
    # 主逻辑
    while True:
        # 调用初始化函数
        possibleValue = init()
        # 调用规则1剪枝
        pruningByRule1(possibleValue)
        # 调用规则2剪枝
        pruningByRule2(possibleValue)
        # display(possibleValue)

        possibleValue = searchForPruning(get_lowest_node(possibleValue), possibleValue)
        # 判断是否有解
        if possibleValue is not None:
            display(possibleValue)
        else:
            print("无解")

        if not judge_result(possibleValue):
            print("结果异常")

        c += 1
        if c >= 90:
            break
    end = time.time()
    print(end - start)


# 读取本地存储文件
infile = open("D:/top95.txt")
main()

扩展

  数独求解的算法,上面这种并不是最快的,还有一种叫做舞蹈链(Dancing Links)的算法,效率更高,有兴趣的可以了解一下;

原文地址:https://www.cnblogs.com/tuyang1129/p/12103726.html