高效算法求解数独



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

时间: 2024-10-11 12:34:25

高效算法求解数独的相关文章

求解数独回溯算法

实现的java代码如下: //判断a[i][j]取值val是否有效 public boolean isValid(int[][] a, int i, int j, int val){ //判断是否跟同行冲突 for(int j1=0;j1<9;j1++){ if(a[i][j1]==val) return false; } //判断是否跟同列冲突 for(int i1=0;i1<9;i1++){ if(a[i1][j]==val) return false; } //找出a[i][j]所在的九

回溯法求解数独算法(C语言)

没有对输入的待解数独进行一般性验证(同一行.一列以及同一个小九宫格都不能出现重复数字) 算法利用回溯的思想: 从第一个空白处开始,找到其候选解(排除同行.同列以及同一小九宫格的所有出现过的数字,剩下未出现的数字都是候选解)的第一个值填入数独. 对第二个空白执行第一步(前面所填入的数字对此空白处有影响). 当出现某个空白的候选解个数为0时,就开始回溯,找到第一个候选解多于一个的,将其在使用的候选解设为不可取(本程序取值为-1),找到其下一个候选解,继续上面的步骤! 直到所有空白处填满,运算完成,输

跳跃的舞者,舞蹈链(Dancing Links)算法——求解精确覆盖问题(转)

跳跃的舞者,舞蹈链(Dancing Links)算法——求解精确覆盖问题 转:http://www.cnblogs.com/grenet/p/3145800.html 精确覆盖问题的定义:给定一个由0-1组成的矩阵,是否能找到一个行的集合,使得集合中每一列都恰好包含一个1 例如:如下的矩阵 就包含了这样一个集合(第1.4.5行) 如何利用给定的矩阵求出相应的行的集合呢?我们采用回溯法 矩阵1: 先假定选择第1行,如下所示: 如上图中所示,红色的那行是选中的一行,这一行中有3个1,分别是第3.5.

斐波那契高效算法(4种算法综合分析)

斐波那契数列问题是算法学习者必定接触到的问题.作为经典问题,首次接触时通常是作为递归算法的案例教程. 然而递归解决斐波那契.其效率低的令人发指,有人算出其时间复杂度为O(2^n).指数级时间复杂度. 假设面试的时候面试官问你斐波那契的求解方法,你来一个递归求解,基本上能够说,你已经game over了. 那么有没有更高效的算法呢.本文将一一介绍. 以下是斐波那契的4种解法: 1.递归    时间复杂度O(2^n) int f(int n){ if(n == 1 || n == 2){ retur

[LeetCode] Sudoku Solver 求解数独

Write a program to solve a Sudoku puzzle by filling the empty cells. Empty cells are indicated by the character '.'. You may assume that there will be only one unique solution. A sudoku puzzle... ...and its solution numbers marked in red. 这道求解数独的题是在之

[LeetCode] 37. Sudoku Solver 求解数独

Write a program to solve a Sudoku puzzle by filling the empty cells. A sudoku solution must satisfy all of the following rules: Each of the digits 1-9 must occur exactly once in each row. Each of the digits 1-9 must occur exactly once in each column.

两种改进的模拟退火算法求解大值域约束满足问题1.0

0引言 约束满足问题(Constraint Satisfaction Problem,CSP)是人工智能研究领域中一个非常重要的分支,现已成为理论计算机科学.数学和统计物理学等交叉学科研究中的热点问题.人工智能.计算机科学和自动控制等领域中的许多问题都可以归结为约束满足问题.同时,约束满足问题在实际问题如模式识别.决策支持.物流调度及资源分配等领域也有着非常广泛的应用. CSP由一个变量集合和一个约束集合组成.每个变量都有一个非空的可能值域,每个约束描述了一个变量子集与子集内各变量的相容赋值,所

秦九韶算法求解多项式

秦九韶算法是中国南宋时期的数学家秦九韶提出的一种多项式简化算法.在西方被称作霍纳算法.它是一种将一元n次多项式的求值问题转化为n个一次式的算法. 一般地,一元n次多项式的求值需要经过[n(n+1)]/2次乘法和n次加法,而秦九韶算法只需要n次乘法和n次加法.其大大简化了计算过程,即使在现代,利用计算机解决多项式的求值问题时,秦九韶算法依然是最优的算法. 题目:写程序计算给定多项式在给定点x处的值 f(x) = a0 + a1x + … + an-1xn-1 + anxn 分析:对比使用常规算法和

蚁群算法求解迷宫最优路径问题

本段程序的基本思想是利用蚁群算法中的蚁周模型,来对全局的迷宫图进行信息素的跟新 和为每一只蚂蚁选择下一个方格. 一共会进行RcMax = 2000轮模拟(理论上模拟的次数越多结果 会越接近真实值),而在每一轮中会排除 M = 10只蚂蚁进行探路.同时在算法的回溯思想上采用的 是栈的数据结构来实现的.当栈最终为空时则表示无解.但同时这段程序的一缺点就是:由于我没在 算法中对每一轮的每只探路蚂蚁采用多线程的模式,所以整体的运行效率还不是很高.如读者有好的 思想或建议,请留言. #include<io