java课程 数独 文库_一次数独生成及求解方案的剖析(Java实现)
数独生成及求解方案剖析(Java实现)
关键词
数独9x9
数独生成
数独解题
序言
最近业务在巩固Java基础,编写了一个基于JavaFX的数独小游戏(随后放链接)。写到核心部分发现平时玩的数独这个东西,还真有点意思:
行、列、子宫格之间的数字互相影响,牵一发而动全身,一不留神就碰撞冲突了,简直都能搞出玄学的意味,怪不得古人能由此“九宫格”演绎出八卦和《周易》。
于是自己想了不少算法,也查找了不少资料,但是都没有找到理想的Java实现;最后无意间在Github发现一个国外大佬写了这样一个算法,体味一番,顿觉精辟!
本篇就是把国外大佬的这个算法拿过来,进行一个深入的解析,希望能帮助到用得上的人。
正文
先上地址
代码只有三个类:
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 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 rowNeighbors;
// 列其他格子列表
private Collection columnNeighbors;
// 子宫格其他格子列表
private Collection 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> rows = new ArrayList<>();
List> columns = new ArrayList<>();
List> boxes = new ArrayList<>();
// 初始化List 9行 9列 9子宫格
for (int i = 0; i < 9; i++) {
rows.add(new ArrayList());
columns.add(new ArrayList());
boxes.add(new ArrayList());
}
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 row = rows.get(i);
for (Cell cell : row) {
List 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 getRowValuesOf(Cell cell) {
List 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> row = new ArrayList<>();
List> 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都不用手动再去修改;每次对各个维度数字进行判断的时候,也就都是在对原始数格进行遍历;其空间复杂度没有增加。
总结
使用递归+随机数组进行枚举和试错——逻辑简明高效
使用List+对象构建数独格子(行、列、3x3子宫格)各维度关联
使用List遍历和排查重复——方法调用简单,引用完美控制了空间复杂度
分析到此,与其说是算法,不如说是对Java对象的构建,通过对Java对象的有效构建,来高效、简便的完成了一次数独的生成和求解。
这便是面向对象代码构建的独到之处!
妙哉妙哉!
java课程 数独 文库_一次数独生成及求解方案的剖析(Java实现)相关推荐
- java课程 数独 文库_通俗易懂的数独算法(java版)
数独算法 一 知识背景 无 二 绪言 偶尔玩下休闲益智小游戏,一方面可以舒解下心情,另一方面刺激下大脑皮层.百度了一下数独的起源和概念.说了那么多,看着就累.精简一下就是数字(0-9)填充游戏.不明白 ...
- java课程 数独 文库_数独java
数独游戏的算法研究与实现_IT/计算机_专业资料.数独游戏的算法研究与实现 java lSSN1009-3044 ComputerKnowledgeAnd MnoJ;01∥电奠知识与技术 E-mail ...
- java家具网站需求分析_基于jsp的家具销售-JavaEE实现家具销售 - java项目源码
基于jsp+servlet+pojo+mysql实现一个javaee/javaweb的家具销售, 该项目可用各类java课程设计大作业中, 家具销售的系统架构分为前后台两部分, 最终实现在线上进行家具 ...
- java招生系统项目_基于jsp的招生管理-JavaEE实现招生管理 - java项目源码
基于jsp+servlet+pojo+mysql实现一个javaee/javaweb的招生管理, 该项目可用各类java课程设计大作业中, 招生管理的系统架构分为前后台两部分, 最终实现在线上进行招生 ...
- java会员卡管理系统下载_基于jsp的会员卡管理系统-JavaEE实现会员卡管理系统 - java项目源码...
基于jsp+servlet+pojo+mysql实现一个javaee/javaweb的会员卡管理系统, 该项目可用各类java课程设计大作业中, 会员卡管理系统的系统架构分为前后台两部分, 最终实现在 ...
- java 公交管理系统 代码_基于jsp的公交管理系统-JavaEE实现公交管理系统 - java项目源码...
基于jsp+servlet+pojo+mysql实现一个javaee/javaweb的公交管理系统, 该项目可用各类java课程设计大作业中, 公交管理系统的系统架构分为前后台两部分, 最终实现在线上 ...
- 基于java的校园新闻_基于jsp的校园新闻-JavaEE实现校园新闻 - java项目源码
基于jsp+servlet+pojo+mysql实现一个javaee/javaweb的校园新闻, 该项目可用各类java课程设计大作业中, 校园新闻的系统架构分为前后台两部分, 最终实现在线上进行校园 ...
- java如何模拟抢单_基于jsp的抢单兼职-JavaEE实现抢单兼职 - java项目源码
基于jsp+servlet+pojo+mysql实现一个javaee/javaweb的抢单兼职, 该项目可用各类java课程设计大作业中, 抢单兼职的系统架构分为前后台两部分, 最终实现在线上进行抢单 ...
- java做同学录管理系统_基于jsp的同学录管理系统-JavaEE实现同学录管理系统 - java项目源码...
基于jsp+servlet+pojo+mysql实现一个javaee/javaweb的同学录管理系统, 该项目可用各类java课程设计大作业中, 同学录管理系统的系统架构分为前后台两部分, 最终实现在 ...
最新文章
- 多线程编程之线程同步主要函数一览
- 高性能服务器-Nginx
- 项目经理要提升需求分析的能力
- Lightroom Classic 教程,如何在 Lightroom 中裁剪并修齐照片?
- Deepgreen DB 是什么(含Deepgreen和Greenplum下载地址)
- win10到优麒麟到深度系统
- hc05模块android代码,Arduino使用HC05蓝牙模块与手机连接
- BugKu CTF(杂项篇MISC)--Pokergame
- 【游戏】——微信打飞机
- 【kubernetes】k8s使用客户端连接haproxy访问高可用集群流程详细说明【使用kubeconfig连接haproxy】【kubeconfig配置全部流程】
- 乐变热更新SDK的集成
- python3.8安装mayavi4.7.3
- NSFC 国家自然科学基金查询
- 大数据及人工智能基础系列2 B+树
- UE4 C++入门——动画蒙太奇 学习笔记
- 计算机教师访谈报告,计算机老师述职报告
- Linux系统目录树结构以及解释
- 1分钟推荐一个深度学习领域的必备网站,高能!
- 配置案例-Profinet转Modbus485网关连接富士电机温控器
- python在水文领域中的应用_python在水利工程或者水文方向上有什么案例可以学习一下么?或者还要学什么库。?...