一次数独生成及解题算法的剖析(Java实现)
数独生成及解题算法剖析(Java实现)
关键词
- 数独9x9
- 数独生成算法
- 数独解题算法
序言
最近业务在巩固Java基础,编写了一个基于JavaFX的数独小游戏(随后放链接)。写到核心部分发现平时玩的数独这个东西,还真有点意思:
行、列、子宫格之间的数字互相影响,牵一发而动全身,一不留神就碰撞冲突了,简直都能搞出玄学的意味,怪不得古人能由此“九宫格”演绎出八卦和《周易》。
于是自己想了不少算法,也查找了不少资料,但是都没有找到理想的Java实现;最后无意间在Github发现一个国外大佬写了这样一个算法,体味一番,顿觉精辟!
本篇就是把国外大佬的这个算法拿过来,进行一个深入的解析,希望能帮助到用得上的人。
正文
先上地址
数独算法Github地址:https://github.com/a11n/sudoku
数独算法Github中文注解地址:https://github.com/JobsLeeGeek/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方法实现,代码我已经做了中文注释:
/**
* 求解方法
*
* @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) {
// 基础校验
verifyGrid(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<Cell> column = columns.get(i);
for (Cell cell : column) {
List<Cell> columnNeighbors = new ArrayList<>(column);
columnNeighbors.remove(cell);
cell.setColumnNeighbors(columnNeighbors);
}
// 逐子宫格
List<Cell> box = boxes.get(i);
for (Cell cell : box) {
List<Cell> boxNeighbors = new ArrayList<>(box);
boxNeighbors.remove(cell);
cell.setBoxNeighbors(boxNeighbors);
}
}
return new Grid(cells);
}
看完代码,其实不难发现,算法不是很复杂,简洁易懂——通过随机和递归进行枚举和试错;
于是本人通过使用基本数据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都不用手动再去修改;每次对各个维度数字进行判断的时候,也就都是在对原始数格进行遍历;其空间复杂度没有增加。
这便是上面代码构建的独到之处!
妙哉妙哉!
一次数独生成及解题算法的剖析(Java实现)的更多相关文章
- 2017BUAA软工个人项目之数独生成与求解
1.项目GitHub地址:https://github.com/ZiJiaW/Soduko (由于一开始把sudoku看成了soduko,于是名字建错了,读起来可能有点奇怪…) 2.项目PSP表格如下 ...
- 微信技术分享:微信的海量IM聊天消息序列号生成实践(算法原理篇)
1.点评 对于IM系统来说,如何做到IM聊天消息离线差异拉取(差异拉取是为了节省流量).消息多端同步.消息顺序保证等,是典型的IM技术难点. 就像即时通讯网整理的以下IM开发干货系列一样: <I ...
- 封装各种生成唯一性ID算法的工具类
/** * Copyright (c) 2005-2012 springside.org.cn * * Licensed under the Apache License, Version 2.0 ( ...
- 常见排序算法题(java版)
常见排序算法题(java版) //插入排序: package org.rut.util.algorithm.support; import org.rut.util.algorithm.Sor ...
- 利用oxygen编辑并生成xml文件,并使用JAVA的JAXB技术完成xml的解析
首先下载oxygen软件(Oxygen XML Editor),目前使用的是试用版(可以安装好软件以后get trial licence,获得免费使用30天的权限,当然这里鼓励大家用正版软件!!!) ...
- Dijkstra算法求最短路径(java)(转)
原文链接:Dijkstra算法求最短路径(java) 任务描述:在一个无向图中,获取起始节点到所有其他节点的最短路径描述 Dijkstra(迪杰斯特拉)算法是典型的最短路径路由算法,用于计算一个节点到 ...
- 排序算法总结(基于Java实现)
前言 下面会讲到一些简单的排序算法(均基于java实现),并给出实现和效率分析. 使用的基类如下: 注意:抽象函数应为public的,我就不改代码了 public abstract class Sor ...
- 八大排序算法总结与java实现(转)
八大排序算法总结与Java实现 原文链接: 八大排序算法总结与java实现 - iTimeTraveler 概述 直接插入排序 希尔排序 简单选择排序 堆排序 冒泡排序 快速排序 归并排序 基数排序 ...
- 第二章:排序算法 及其他 Java代码实现
目录 第二章:排序算法 及其他 Java代码实现 插入排序 归并排序 选择排序算法 冒泡排序 查找算法 习题 2.3.7 第二章:排序算法 及其他 Java代码实现 --算法导论(Introducti ...
随机推荐
- FL Studio采样设置之时间拉伸栏
今天小编将带领大家了解一下FL Studio采样设置页面中的时间拉伸栏知识,该栏目包含了和采样音频的时间拉伸相关的设置.其右边是一个时间伸缩方式下拉列表,里面列出了很多种类的伸缩方式,自动方式是默认的 ...
- 怎么用在线思维导图Ayoa规划个人任务
在Ayoa的任务板功能中可以对某一任务进行详细设置,例如改变紧急情况/重要程度.添加到我的计划工具.设置开始日期.截止日期等. 图1:任务详情设置 而这里的"我的计划工具"就是一个 ...
- 如何用pdfFactory新建打印机并设置属性
今天我们来讲一讲,在pdfFactory中如何去修改PDF文件打印页面的页边距.页面大小.页面清晰度等属性参数. pdfFactory是一款Windows平台上的虚拟打印机,在没有打印机可以安装的情况 ...
- 【性能测试】【locust】快速入门
简介 locust是一个开源的分布式用户负载压力测试工具,对网站(其他系统,例如接口等)进行负载测试,并确定可以处理多少的并发用户,locust特点利用了协程支持,达到高数量级别的并发,以及基于事件驱 ...
- 企业安全03Django GIS SQL注入漏洞CVE-2020-9402
Django GIS SQL注入漏洞CVE-2020-9402 一.漏洞描述 Django是Django基金会的一套基于Python语言的开源Web应用框架.该框架包括面向对象的映射器.视图系统.模板 ...
- 【mq读书笔记】mq消息发送
钩子的注册: DefaultMQProducerImpl#registerSendMessageHook注册钩子处理类,可注册多个. public SendResult sendMessage( fi ...
- GAN和GAN的改进
GAN 原始GAN中判别器要最小化如下损失函数,尽可能把真实样本分为正例,生成样本分为负例: 其中是真实样本分布,是由生成器产生的样本分布. 第一个式子我们不看梯度符号的话即为判别器的损失函数,log ...
- Network-Emulator-Toolkit 模拟各种网络环境 windows
背景.目标.目的 (1) 背景: 我们在使用网络时,时常遇到在正常网络环境下的代码运行一切正常,可以复杂的网络环境下的各种问题无法复现,必须搭建模拟各种网络环境,去复现问题,定位问题.不管是移动平台, ...
- Cisco交换机常见配置
-------查看当前系统基础信息-------- sh version //查看当前IOS版本. sh running-config //查看当前系统中运行的配置信息 -------清除当前系统配置 ...
- houdini 鱼眼相机
http://mattebb.com/weblog/houdini-fisheye-camera/ 这个网站是有提供一个相机shader的,,如图是方形的,国内的用户,比较多是做球幕的小伙伴,圆形就行 ...