zoukankan      html  css  js  c++  java
  • 一次数独生成及求解方案的剖析(Java实现)

    数独生成及求解方案剖析(Java实现)

    关键词

    • 数独9x9
    • 数独生成
    • 数独解题

    序言

    最近业务在巩固Java基础,编写了一个基于JavaFX的数独小游戏(链接点我)。写到核心部分发现平时玩的数独这个东西,还真有点意思:

    行、列、子宫格之间的数字互相影响,牵一发而动全身,一不留神就碰撞冲突了,简直都能搞出玄学的意味,怪不得古人能由此“九宫格”演绎出八卦和《周易》。

    于是自己想了不少算法,也查找了不少资料,但是都没有找到理想的Java实现;最后无意间在Github发现一个国外大佬写了这样一个算法,体味一番,顿觉精辟!

    本篇就是把国外大佬的这个算法拿过来,进行一个深入的解析,希望能帮助到用得上的人。


    正文

    先上地址

    数独算法Github地址:https://github.com/a11n/sudoku

    数独算法Github中文注解地址:https://github.com/JobsLeeCN/sudoku

    代码只有三个类:

    • Generator.java

    生成器 -> 生成数独格子

    • Solver.java

    解法器 -> 数独求解

    • Grid.java

    网格对象 -> 基础数独格子对象

    直接上main方法看下基本调用:

    public static void main(String[] args) {
            // 生成一个20个空格的9x9数独
            Generator generator = new Generator();
            Grid grid = generator.generate(20);
            System.out.println(grid.toString());
            // 9x9数独求解
            Solver solver = new Solver();
            solver.solve(grid);
            System.out.println(grid.toString());
        }
    

    看下输出结果(输出方法我自己进行了修改):

    生成的9x9数独(0为空格)

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

    数独求解

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

    使用起来很简单,速度也很快;其核心部分的代码,其实只有三个点。

    1. 第一点 解法

    • 随机数组
    • 递归填数

    在Solver.java中solve方法实现;

    每次遍历的是使用交换方法实现的随机数组,保证了随机数组空间的有限占用,并且能够减少枚举过程中的重复几率。

    代码我已经做了中文注释:

    /**
     * 获取随机数组
     *
     * @return
     */
    private int[] generateRandomValues() {
        // 初始化随机数组 此处空格子0是因为格子初始化的时候 默认给的就是0 便于判断和处理
        int[] values = {EMPTY, 1, 2, 3, 4, 5, 6, 7, 8, 9};
        Random random = new Random();
        // 使用交换法构建随机数组
        for (int i = 0, j = random.nextInt(9), tmp = values[j];
             i < values.length;
             i++, j = random.nextInt(9), tmp = values[j]) {
            if (i == j) continue;
            values[j] = values[i];
            values[i] = tmp;
        }
        return values;
    }
    
    /**
     * 求解方法
     *
     * @param grid
     * @param cell
     * @return
     */
    private boolean solve(Grid grid, Optional<Grid.Cell> cell) {
        // 空格子 说明遍历处理完了
        if (!cell.isPresent()) {
            return true;
        }
        // 遍历随机数值 尝试填数
        for (int value : values) {
            // 校验填的数是否合理 合理的话尝试下一个空格子
            if (grid.isValidValueForCell(cell.get(), value)) {
                cell.get().setValue(value);
                // 递归尝试下一个空格子
                if (solve(grid, grid.getNextEmptyCellOf(cell.get()))) return true;
                // 尝试失败格子的填入0 继续为当前格子尝试下一个随机值
                cell.get().setValue(EMPTY);
            }
        }
        return false;
    }
    

    2. 第二点 构建

    • 对象数组

    整个对象的构建在Grid.java中,其中涉及到两个对象Grid和Cell,Grid由Cell[][]数组构成,Cell中记录了格子的数值、行列子宫格维度的格子列表及下一个格子对象:

    Grid对象

    /**
     * 由数据格子构成的数独格子
     */
    private final Cell[][] grid;
    

    Cell对象

    // 格子数值
    private int value;
    // 行其他格子列表
    private Collection<Cell> rowNeighbors;
    // 列其他格子列表
    private Collection<Cell> columnNeighbors;
    // 子宫格其他格子列表
    private Collection<Cell> boxNeighbors;
    // 下一个格子对象
    private Cell nextCell;
    

    3. 第三点 遍历判断

    • 多维度引用
    • 判断重复

    Grid初始化时,在Cell对象中,使用List构造了行、列、子宫格维度的引用(请注意这里的引用,后面会讲到这个引用的妙处),见如下代码及中文注释:

    /**
     * 返回数独格子的工厂方法
     *
     * @param grid
     * @return
     */
    public static Grid of(int[][] grid) {
        ...
    
        // 初始化格子各维度统计List 9x9 行 列 子宫格
        Cell[][] cells = new Cell[9][9];
        List<List<Cell>> rows = new ArrayList<>();
        List<List<Cell>> columns = new ArrayList<>();
        List<List<Cell>> boxes = new ArrayList<>();
        // 初始化List 9行 9列 9子宫格
        for (int i = 0; i < 9; i++) {
            rows.add(new ArrayList<Cell>());
            columns.add(new ArrayList<Cell>());
            boxes.add(new ArrayList<Cell>());
        }
    
        Cell lastCell = null;
        // 逐一遍历数独格子 往各维度统计List中填数
        for (int row = 0; row < grid.length; row++) {
            for (int column = 0; column < grid[row].length; column++) {
                Cell cell = new Cell(grid[row][column]);
                cells[row][column] = cell;
    
                rows.get(row).add(cell);
                columns.get(column).add(cell);
                // 子宫格在List中的index计算
                boxes.get((row / 3) * 3 + column / 3).add(cell);
                // 如果有上一次遍历的格子 则当前格子为上个格子的下一格子
                if (lastCell != null) {
                    lastCell.setNextCell(cell);
                }
                // 记录上一次遍历的格子
                lastCell = cell;
            }
        }
    
        // 逐行 逐列 逐子宫格 遍历 处理对应模块的关联邻居List
        for (int i = 0; i < 9; i++) {
            // 逐行
            List<Cell> row = rows.get(i);
            for (Cell cell : row) {
                List<Cell> rowNeighbors = new ArrayList<>(row);
                rowNeighbors.remove(cell);
                cell.setRowNeighbors(rowNeighbors);
            }
    
            // 逐列
            ...
    
            // 逐子宫格
            ...
        }
    
        ...
    }
    

    构造完成后,每试一次填数,就遍历一次多维度的List判断行、列、3x3子宫格的数字是否重复:

    /**
     * 判断格子填入的数字是否合适
     *
     * @param cell
     * @param value
     * @return
     */
    public boolean isValidValueForCell(Cell cell, int value) {
        return isValidInRow(cell, value) && isValidInColumn(cell, value) && isValidInBox(cell, value);
    }
    
    ...
    
    /**
     * 判断数独行数字是否合规
     *
     * @param cell
     * @param value
     * @return
     */
    private boolean isValidInRow(Cell cell, int value) {
        return !getRowValuesOf(cell).contains(value);
    }
    
    ...
    
    /**
     * 获取行格子数值列表
     *
     * @param cell
     * @return
     */
    private Collection<Integer> getRowValuesOf(Cell cell) {
        List<Integer> rowValues = new ArrayList<>();
        for (Cell neighbor : cell.getRowNeighbors()) rowValues.add(neighbor.getValue());
        return rowValues;
    }
    

    看完代码,其实不难发现,算法不是很复杂,简洁易懂——通过随机和递归进行枚举和试错,外加List.contains()方法遍历判断;逻辑并不复杂,代码也十分精炼;

    于是本人通过使用基本数据int[][],不使用对象,按照其核心逻辑实现了自己的一套数独,却发现极度耗时(大家可以自己尝试下),很久没有结果输出。

    为什么同样是递归,自己的性能却这么差呢?

    仔细思考,最后发现面向对象真的是个好东西,例子中的对象的引用从很大一层面上解决了本方法数独递归的性能问题。


    写一个有趣的例子来解释下,用一个对象构建二维数组,初始化数值后,分别按照行维度和列维度关联到对应的List中,打印数组和这些List;

    然后我们修改(0,0)位置的数值,注意,这里不是new一个新的对象,而是直接使用对象的set方法操作其对应数值,再打印数组和这些List,代码和结果如下:

    示例代码

    public static void main(String[] args) {
            Entity[][] ee = new Entity[3][3];
            for (int i = 0; i < 3; i++) {
                for (int j = 0; j < 3; j++) {
                    Entity e = new Entity();
                    e.setX(i);
                    e.setY(j);
                    ee[i][j] = e;
                }
            }
            System.out.println(Arrays.deepToString(ee));
    
            List<List<Entity>> row = new ArrayList<>();
            List<List<Entity>> column = new ArrayList<>();
            for (int i = 0; i < 3; i++) {
                row.add(new ArrayList<>());
            }
            for (int i = 0; i < 3; i++) {
                for (int j = 0; j < 3; j++) {
                    row.get(i).add(ee[i][j]);
                }
            }
            for (int j = 0; j < 3; j++) {
                column.add(new ArrayList<>());
            }
            for (int j = 0; j < 3; j++) {
                for (int i = 0; i < 3; i++) {
                    column.get(j).add(ee[i][j]);
                }
            }
            System.out.println(row);
            System.out.println(column);
    
            System.out.println("");
    
            ee[0][0].setX(9);
            ee[0][0].setY(9);
            System.out.println(Arrays.deepToString(ee));
            System.out.println(row);
            System.out.println(column);
        }
    
        static class Entity {
            private int x;
            private int y;
    
            public int getX() {
                return x;
            }
    
            public void setX(int x) {
                this.x = x;
            }
    
            public int getY() {
                return y;
            }
    
            public void setY(int y) {
                this.y = y;
            }
    
            @Override
            public String toString() {
                return "Entity{" +
                        "x=" + x +
                        ", y=" + y +
                        '}';
            }
        }
    

    输出结果

    [[Entity{x=0, y=0}, Entity{x=0, y=1}, Entity{x=0, y=2}], [Entity{x=1, y=0}, Entity{x=1, y=1}, Entity{x=1, y=2}], [Entity{x=2, y=0}, Entity{x=2, y=1}, Entity{x=2, y=2}]]
    [[Entity{x=0, y=0}, Entity{x=0, y=1}, Entity{x=0, y=2}], [Entity{x=1, y=0}, Entity{x=1, y=1}, Entity{x=1, y=2}], [Entity{x=2, y=0}, Entity{x=2, y=1}, Entity{x=2, y=2}]]
    [[Entity{x=0, y=0}, Entity{x=1, y=0}, Entity{x=2, y=0}], [Entity{x=0, y=1}, Entity{x=1, y=1}, Entity{x=2, y=1}], [Entity{x=0, y=2}, Entity{x=1, y=2}, Entity{x=2, y=2}]]
    
    [[Entity{x=9, y=9}, Entity{x=0, y=1}, Entity{x=0, y=2}], [Entity{x=1, y=0}, Entity{x=1, y=1}, Entity{x=1, y=2}], [Entity{x=2, y=0}, Entity{x=2, y=1}, Entity{x=2, y=2}]]
    [[Entity{x=9, y=9}, Entity{x=0, y=1}, Entity{x=0, y=2}], [Entity{x=1, y=0}, Entity{x=1, y=1}, Entity{x=1, y=2}], [Entity{x=2, y=0}, Entity{x=2, y=1}, Entity{x=2, y=2}]]
    [[Entity{x=9, y=9}, Entity{x=1, y=0}, Entity{x=2, y=0}], [Entity{x=0, y=1}, Entity{x=1, y=1}, Entity{x=2, y=1}], [Entity{x=0, y=2}, Entity{x=1, y=2}, Entity{x=2, y=2}]]
    
    

    神奇的地方就在这里,行列关联的List里面的数值跟随着一起改变了。

    这是为什么呢?

    Java的集合中存放的类型

    (1)如果是基本数据类型,则是value;

    (2) 如果是复合数据类型,则是引用的地址;

    List中放入对象时,实际放入的不是对象本身而是对象的引用;

    对象数组只需要自己占据一部分内存空间,List来引用对象,就不需要额外有数组内存的开支;

    同时对原始数组中对象的修改(注意,修改并非new一个对象,因为new一个就开辟了新的内存地址,引用还会指向原来的地址),就可以做到遍历一次、处处可见了!


    由此画一张实体与引用关系图:

    这样以来,数组内存还是原来的一块数组内存,我们只需用List关联引用,就不用需要每次遍历和判断的时候开辟额外空间了;

    然后每次对原始数格处理的时候,其各个维度List都不用手动再去修改;每次对各个维度数字进行判断的时候,也就都是在对原始数格进行遍历;其空间复杂度没有增加。


    总结

    1. 使用递归+随机数组进行枚举和试错——逻辑简明高效
    2. 使用List+对象构建数独格子(行、列、3x3子宫格)各维度关联
    3. 使用List遍历和排查重复——方法调用简单,引用完美控制了空间复杂度

    分析到此,与其说是算法,不如说是对Java对象的构建,通过对Java对象的有效构建,来高效、简便的完成了一次数独的生成和求解。

    这便是面向对象代码构建的独到之处!

    妙哉妙哉!

    做一个仰望星空的极客
  • 相关阅读:
    Directx11教程(63) tessellation学习(5)
    OpenCL 学习step by step (1) 安装AMD OpenCL APP
    Directx11教程(62) tessellation学习(4)
    Directx11教程(61) tessellation学习(3)
    Notes 和 Domino 已知限制
    asp.net中的WebApplication和WebSite
    平淡的2007
    TinyMCE与Domino集成
    jOOQ 2.6.0 发布,Java的ORM框架
    bandwidth 0.32f 发布,内存带宽测试工具
  • 原文地址:https://www.cnblogs.com/aqiu18/p/14155182.html
Copyright © 2011-2022 走看看