数独题的生成与解决方法

前言

最近在学习Java,在梁勇的 Introduction to Java Programming 10ed 中看到了一个数独问题的例子,这个例子其实是引导学习二维数组的例子,书本中给出的例子也比较简单,就是判断一个数独答案是不是正确的。

其实进行到这,学习知识的目的已经达到了,但是只能输入一个数独答案判断一下是否正确,这实在是太太太太太傻了,不知道有多傻。我始终按耐不住心中那股探索欲,我要做一个生成数独题的程序,同时它还能自己解决。于是这就开启了潘多拉的魔盒。

背景

数独是一种源自18世纪末的瑞士,后在美国发展,并在日本得以发扬光大的数学智力拼图游戏,其游戏规则为:在由9个小九宫格组成的大九宫格里,已经填有若干数字,需用数字1~9填满剩下的空格,使得

  1. 每行9个格子填入9个不同的数字
  2. 每列9个格子填入9个不同的数字
  3. 每宫9个格子填入9个不同的数字
问题                  答案
0 0 0 0 0 0 0 0 0    1 2 3 4 6 5 7 8 9
0 0 0 0 0 0 1 6 2    4 5 7 3 8 9 1 6 2
0 0 0 0 2 7 0 0 3    8 6 9 1 2 7 4 5 3
0 0 4 0 0 1 0 0 0    3 7 4 5 9 1 6 2 8
0 0 0 0 0 0 3 9 0    5 8 1 6 7 2 3 9 4
0 0 6 0 3 4 0 0 0    2 9 6 8 3 4 5 1 7
0 4 0 0 0 0 0 0 1    6 4 8 2 5 3 9 7 1
0 0 5 0 4 8 2 0 6    7 1 5 9 4 8 2 3 6
0 3 0 7 1 6 8 0 0    9 3 2 7 1 6 8 4 5

难度等级的度量

对于我这个数独游戏的门外汉,我只能通过感性认识来度量一道数独题的难度。

一个人类解决一道数独题是在已有的信息之上来解决的,已有的信息包括剩余数字的数量以及数字的分布。一个数独题中,剩余数字的数量以及数字分布的均匀、对称性是决定问题难度的关键。因此可以通过两个衡量因素:数字个数、数字分布,来衡量一个数独问题的难度。难度可以这样划分:

  1. 已知格总数
  2. 行中已知格数
  3. 列中已知格数

那么问题来了,一道题最少可以留下几个格子,人们才有可能解决呢?这个问题目前仍无定案,不过听数学家说是17个,不过那将是骨灰级难度了。一般来说,数独题是在22~30个左右。因此我就把数独题设置成这个样子。

其次,就是数字的分布,从出题者的角度看,数字的分布也就是在一个数独答案之上选择按照什么顺序挖洞(把某个数字挖掉),为了使得剩余数字分布均匀一些,可以随机挖洞,或者隔开一个挖一个。为了把难度加大,就让剩余数字分布不均匀一些,比方说按照从左到右从上到下的顺序挖洞。嘻嘻,我就是这样干的。

其实还可以通过写程序解决问题,并且统计解决时间来衡量一个问题的难度。不过那就是研究数独的人干的事儿了,我们是Coder,只需要在脑子里有一个难度的印象就行了。

算法分析

我们的目标是让程序生成一道题,并且自己解决这道题。

求解算法

这里我采用的是深度优先搜索的方式解决一道题,算法从上到下,从左到右依次尝试填入每个数字,最终寻找出正确解决,十分暴力。

    /*
     * DFS解数独问题
     */
    public static boolean dfs(int[][] f, boolean[][] r, boolean[][] c, boolean[][] b) {
        for(int i = 0; i < 9; i++)
            for(int j = 0; j < 9; j++)
                if(f[i][j] == 0) {
                    int k = i / 3 * 3 + j / 3;
                    // 尝试填入1~9
                    for(int n = 1; n < 10; n++) {
                        if(!r[i][n] && !c[j][n] && !b[k][n]) {
                            // 尝试填入一个数
                            r[i][n] = true;
                            c[j][n] = true;
                            b[k][n] = true;
                            f[i][j] = n;
                            // 检查是否满足数独正解
                            if(dfs(f, r, c, b))
                                return true;
                            // 不满足则回溯
                            r[i][n] = false;
                            c[j][n] = false;
                            b[k][n] = false;
                            f[i][j] = 0;
                        }
                    }
                    // 尝试所有数字都不满足则回溯
                    return false;
                }
        return true;
    }

函数的(f,r,c,b)二维数组分别表示

  • f : 九宫格的数字,f[i][j]的范围是1~9
  • r : r[0][1] = true 表示第0行里已经有1填入了
  • c : c[0][1] = true 表示第0列里已经有1填入了
  • b : b[0][1] = true 表示第0宫里已经有1填入了

利用这4个全局的二维数组可以比较快速的判断当前解决方案的状态是否满足数组的限制条件,其实也可以专门写函数来判断,不过我这算是用空间换时间了。

生成算法

我的生成算法首先使用拉斯维加斯随机算法来生成一个数独答案,是数独答案。然后按照从上到下从左到右的顺序依次挖洞,不过这个挖洞可没那么简单,这一挖还得使得生成的数独题只有唯一解,因此就得多做一步判断唯一解的工作。

    /*
     * 拉斯维加斯随机算法生成一个随机数独问题
     */
    public static boolean lasVegas(int n) {
        int i, j, k, value;
        Random random = new Random();

        // 初始化
        for(i = 0; i < 9; i++) {
            for(j = 0; j < 9; j++) {
                field[i][j] = 0;
                rows[i][j+1] = false;
                cols[i][j+1] = false;
                blocks[i][j+1] = false;
            }
        }

        // 随机填入数字
        while(n > 0) {
            i = random.nextInt(9);
            j = random.nextInt(9);
            if(field[i][j] == 0) {
                k = i / 3 * 3 + j / 3;
                value = random.nextInt(9) + 1;
                if(!rows[i][value] && !cols[j][value] && !blocks[k][value]) {
                    field[i][j] = value;
                    rows[i][value] = true;
                    cols[j][value] = true;
                    blocks[k][value] = true;
                    n--;
                }
            }
        }

        // 检查并且生成一个数组解
        if(dfs(field, rows, cols, blocks))
            return true;
        else
            return false;
    }

拉斯维加斯算法中的n表示随机填入几个位置,你可以自己取值,不过我取的是11,因为取11的时候粗略测量已经有99%的概率生成一个正解了,可以参照:

    public static void main(String[] args) {
        // 拉斯维加斯算法生成数独
        while(!lasVegas(11));

        // 输入剩余数字数
        Scanner input = new Scanner(System.in);
        System.out.print("Enter the level(22 - 30): ");
        int level = input.nextInt();

        while(level < 22 || level > 30) {
            System.out.print("Enter the level(22 - 30): ");
            level = input.nextInt();
        }

        // 生成数独题
        generateByDigMethod(level);
        printer();

        // 提示答案
        System.out.print("Wanan answer ? (input 1): ");
        int hint = input.nextInt();
        if(hint == 1) {
            dfs(field, rows, cols, blocks);
            printer();
        }
    }

那么如果判断唯一解呢?其实用的是反证法的思想,挖掉一个洞,比如是第三行第三个,原来的数字是9,这下我们把它换成1~8,然后让上面的程序解一下。如果它还能解出答案,那么这个问题就有至少两个解了,这就不对了。于是乎我们跳过它,去挖第三行第四个,然后继续判断。最终我们就生成唯一解的题目了!

    /*
     * 挖洞法生成一个数独问题
     * level: 剩余数字
     */
    public static void generateByDigMethod(int level) {
        // 从上到下从左到右的顺序挖洞
        for(int i = 0; i < 9; i++)
            for(int j = 0; j < 9; j++)
                if(checkUnique(i, j)) {
                    int k = i / 3 * 3 + j / 3;
                    rows[i][field[i][j]] = false;
                    cols[j][field[i][j]] = false;
                    blocks[k][field[i][j]] = false;
                    field[i][j] = 0;
                    level++;
                    if(81 == level)
                        break;
                }
    }
    /*
     * 判断唯一解
     * 挖掉[r, c]位置的数字判断是否得到唯一解
     */
    public static boolean checkUnique(int r, int c) {
        // 挖掉第一个位置一定有唯一解
        if(r == 0 && c == 0)
            return true;

        int k = r / 3 * 3 + c / 3;
        boolean[][] trows = new boolean[9][10];
        boolean[][] tcols = new boolean[9][10];
        boolean[][] tblocks = new boolean[9][10];
        int[][] tfield = new int[9][9];

        // 临时数组
        for(int i = 0; i < 9; i++) {
            for(int j = 0; j < 9; j++) {
                trows[i][j+1] = rows[i][j+1];
                tcols[i][j+1] = cols[i][j+1];
                tblocks[i][j+1] = blocks[i][j+1];
                tfield[i][j] = field[i][j];
            }
        }

        // 假设挖掉这个数字
        trows[r][field[r][c]] = false;
        tcols[c][field[r][c]] = false;
        tblocks[k][field[r][c]] = false;

        for(int i = 1; i < 10; i++)
            if(i != field[r][c]) {
                tfield[r][c] = i;
                if(!trows[r][i] && !tcols[c][i] && !tblocks[k][i]) {
                    trows[r][i] = true;
                    tcols[c][i] = true;
                    tblocks[k][i] = true;
                    // 更换一个数字之后检查是否还有另一解
                    if(dfs(tfield, trows, tcols, tblocks))
                        return false;
                    trows[r][i] = false;
                    tcols[c][i] = false;
                    tblocks[k][i] = false;
                }
            }
        // 已尝试所有其他数字发现无解即只有唯一解
        return true;
    }

判断结果正确与否

最后送上一段判断正解的算法,很简单的算法

/**
 * 数独答案检查
 * @author trav
 */
public class CheckSudokuSolution {

    public static void main(String[] args) {
        int[][] grid = readSolution();

        System.out.println(isValid(grid) ? "Valid solution" : "Invalid solution");
    }

    public static int[][] readSolution() {
        Scanner input = new Scanner(System.in);

        System.out.println("Enter a Sudoku puzzle solution:");
        int[][] grid = new int[9][9];
        for(int i = 0; i < 9; i++)
            for(int j = 0; j < 9; j++)
                grid[i][j] = input.nextInt();
        return grid;
    }

    public static boolean isValid(int[][] grid) {
        for(int i = 0; i < 9; i++)
            for(int j = 0; j < 9; j++)
                if(grid[i][j] < 1 || grid[i][j] > 9 || !isValid(i, j, grid))
                    return false;
        return true;
    }

    public static boolean isValid(int i, int j, int[][] grid) {
        // 检查列唯一性
        for(int column = 0; column < 9; column++)
            if(column != j && grid[i][column] == grid[i][j])
                return false;

        // 检查行唯一性
        for(int row = 0; row < 9; row++)
            if(row != i && grid[row][j] == grid[i][j])
                return false;

        // 检查格唯一性
        for(int row = (i / 3) * 3; row < (i / 3) * 3 + 3; row++)
            for(int col = (j / 3) * 3; col < (j / 3) * 3 + 3; col++)
                if(row != i && col != j && grid[row][col] == grid[i][j])
                    return false;

        return true;
    }

}

算法复杂度分析

聪明的读者应该已经发现了,生成算法十分依赖求解算法,因此分析时间复杂度的关键在于调用了多少次求解算法,因为DFS的时间复杂度大家都知道是O(V+E)。

在生成算法中,包括生成一个最终解以及挖洞。生成一个最终解由于采用的是随机算法,因此分析起来比较复杂,不过将n取11的时候已经有99%概率生成正解了,也就是99%的概率只需要尝试一次,因此不妨就设为O(V+E)。

而挖洞的过程中,需要尝试81次,也就是 81 * O(V+E),然而V也就是81,因此时间复杂度是O(V^2),还是挺大的,有待改进。

总结

程序中还有许多可以改进的地方,比如设置难度级别、生成的题目可以进行对称轮换、挖洞的顺序可以按难度分为多种等等。算法时间复杂度还是挺高的,不过还好数独只有81个格子,在我的机子上还是跑得飞快的。

听说多做做数独题可以防止老年痴呆,这下舒服了。

Reference

[1]薛源海,蒋彪彬,李永卓,闫桂峰,孙华飞.基于“挖洞”思想的数独游戏生成算法[J].数学的实践与认识,2009,39(21):1-7.

[2]Sudoku Wikipedia, 2018. https://en.wikipedia.org/wiki/Sudoku

原文地址:https://www.cnblogs.com/trav/p/10197907.html

时间: 2024-08-29 09:59:06

数独题的生成与解决方法的相关文章

axis2的wsdl无法使用eclipse axis1插件来生成客户端--解决方法

使用jetty+axis2实现webservice服务端,且无需使用axis2命令生成服务端代码,只要services.xml配置实现类. 工程为gradle工程配置文件在src/main/resources/axis2/WEB-INF/services.xml: <?xml version="1.0" encoding="UTF-8"?> <serviceGroup> <service name="wifiecService

70-persistent-net.rules无法自动生成,解决方法

无法自动生成70-persistent-net.rules文件的原因: 在更换linux内核前修改ifcfg-eth0文件,更换内核,使用dhclient无法动态分配IP,删掉70-persistent-net.rules文件,重启系统. 在/dev/udev/rules.d文件夹下没有自动生成70-persistent-net.rules文件. 解决方法: 手动执行/lib/udev/write_net_rules 如果提示信息为: missing $INTERFACE 添加变量: expor

spring中配置quartz调用两次及项目日志log4j不能每天生成日志解决方法

在quartz中配置了一个方法运行时会连续调用两次,是因为加载两次,只需在tomcat的server.xml中修改配置 <Host name="www.xx.cn" appBase="" unpackWARs="true" autoDeploy="true"> <Context path="" docBase="webapps/demo" reloadable=&quo

创建发布时无法生成快照,解决方法

引用 主要是要注意权限的问题,一般做发布/订阅,建议你做如下准备工作: 1.发布服务器,订阅服务器都创建一个同名的windows用户,并设置相同的密码,做为发布快照文件夹的有效访问用户 我的电脑 --控制面板 --管理工具 --计算机管理 --用户和组 --右键用户 --新建用户 --建立一个隶属于administrator组的登陆windows的用户 2.在发布服务器上,新建一个共享目录,做为发布的快照文件的存放目录,操作: 我的电脑--D:\ 新建一个目录,名为: PUB --右键这个新建的

9-Unittest+HTMLTestRunner不能生成报告解决方法

1.问题现象 在使用HTMLTestRunner生成测试报告时,出现程序运行不报错,但不能生成报告的情况. 刚开始找了很久没发现问题,后来加上打印信息,发现根本没执行生成报告这部分代码.最后网上找到原因:pycharm 在运行测试用例的时候 默认是以unittest 框架来运行的,所以不能生成测试报告. if __name__ == '__main__': print '开始执行main' # 创建文件,用来存放测试报告 filepath = '..\htmlreport\htmlreport.

Unittest+HTMLTestRunner不能生成报告解决方法和报告安装使用

1.问题现象 在使用HTMLTestRunner生成测试报告时,出现程序运行不报错,但不能生成报告的情况. 刚开始找了很久没发现问题,后来加上打印信息,发现根本没执行生成报告这部分代码.最后网上找到原因:pycharm 在运行测试用例的时候 默认是以unittest 框架来运行的,所以不能生成测试报告. 需要设置成不要用unittest框架运行: HTMLTestRunner.pyw文件经过修改的,需要下载:链接:https://pan.baidu.com/s/1BtF4Xus3kecI8qfT

HDP出现Could not create the Java Virtual Machine解决方法

操作系统:Centos6.6,JDK:1.7 ,HDP:2.2 启动HDP平台时,出现如题的错误,解决方法: 在/etc/profile 中加入: export _JAVA_OPTIONS="-Xms512M -Xmx512M" (当然,上面的512可以根据实际情况进行调整) 出现的原因是因为部署的HDP的服务太多了,资源(内存)不够用导致的. 版权声明:本文为博主原创文章,未经博主允许不得转载.

老男孩教育每日一题-2017年4月28日- MySQL主从复制常见故障及解决方法?

MySQL主从复制常见故障及解决方法? 1.1.1故障1:从库数据与主库冲突 show slave status; 报错:且show slave status\G Slave_I/O_Running:Yes Slave_SQL_Running:No Seconds_Behind_Master:NULL Last_error:Error 'Can't create database 'xiaoliu'; database exists' on query. Default   database:'

Android项目中gen文件下R文件无法生成的解决的方法

帮一个网友解决R文件无法生成的问题,搜集了些材料特整理例如以下,刚開始学习的人參考他人代码时极易出现此种问题,一般都是xml文件出错,无法被正确解析. gen文件夹无法更新,或者gen文件夹下的R.JAVA文件无法生成 1.gen文件夹的用处 android gen文件夹下的R.java并非由用户创建,而是androidproject本身将android的资源进行自己主动"编号"(ID)值. 2.gen文件夹下R文件无法更新/生成的原因 1)res文件夹下的layout下的xml文件名