1. 前言
本文的一些图片, 资料 截取自编程之美
2. 问题描述
额, 4.9 这里的问题是 给定一个残缺的数独, 粗鲁额的计算一下大概有多少种解法, 以及多少种独立的解答, 。。。一看 这个便是一个数学问题,,,
算了 跳过算了, 跳到了1.9, 这个是关于如果解决填充残缺的数独的问题的
其实, 对于数独的解法, 我也是在贴吧看见一个家伙, 在说, 貌似是那家伙要找一个提高他自己的解决数独效率, 然后 后来我自己没事情的时候, 就写了写 思路 是基于书中的第一种思路
3. 问题分析
解法一 : 获取所有的空格子的所有可能的取值, 遍历所有的空格子, 尝试填充所有的可能, 当不符合数独的特性的时候, 进行回溯, 将当前的空格子设置为当前空格的下一种可能的数字, 如果当前空格没有了可能的数字, 则回溯到前一个空格, 设置该空格的值为该空格的下一种可能, 然后继续尝试当前空格的所有可能, 这种思路能够找到所有的解, 但是复杂度较高
解法二 : 非用言语能够表达.. 上图吧
请注意 后面有一句, 这种方法并不能解出所有的情况 !
牺牲了一定程度的正确性
我的思路
我的这里的思路是基于第一种解法的, 不过每次假设值的时候, 找出备选数最少的坐标假设值
4. 代码
1 : Candidate 存储每一个方格的备选数目的数据结构
/**
* file name : Candidates.java
* created at : 7:58:14 PM Apr 22, 2015
* created by 970655147
*/
package com.hx.sudoku02;
// 备选的数据集合
public class Candidate {
// 当前对象 对应的方格的行, 列 以及备选的数据集合
private Integer row;
private Integer col;
private List<Integer> candidates;
// 初始化
public Candidate() {
candidates = new ArrayList<Integer>();
}
public Candidate(List<Integer> candidates, int row, int col) {
this.candidates = candidates;
this.row = row;
this.col = col;
}
// setter & getter
public void putCandidate(Integer candidate) {
this.candidates.add(candidate);
}
public Integer getCandidate() {
return candidates.remove(candidates.size() - 1);
}
public boolean hasCandidates() {
return candidates.size() > 0;
}
public int size() {
return candidates.size();
}
public Integer getRow() {
return row;
}
public Integer getCol() {
return col;
}
// for debug ...
// 如果candidates中数据为[1, 2, 3] 返回1, 2, 3
public String toString() {
if(candidates.size() == 0) {
return "[...]";
}
StringBuilder sb = new StringBuilder(candidates.size() * 3);
for(int i=0; i<candidates.size(); i++) {
sb.append(candidates.get(i) + ", ");
}
return sb.substring(0, sb.length() - 2);
}
// 获取除了给定的val的其他的candidates
public Candidate updateCandidate(int val) {
List<Integer> dstCandidates = new ArrayList<Integer>(candidates.size());
for(int i=0; i<candidates.size(); i++) {
if(candidates.get(i).intValue() != val) {
dstCandidates.add(candidates.get(i));
}
}
return new Candidate(dstCandidates, row, col);
}
// 判定两个Candidate对象相同
public boolean equals(Candidate can) {
for(int i=0; i<candidates.size(); i++) {
if(candidates.get(i).intValue() != can.candidates.get(i).intValue()) {
return false;
}
}
return true;
}
// 复制当前的Candidate对象
public Candidate copy() {
List<Integer> dstCandidates = new ArrayList<Integer>(candidates.size());
for(int i=0; i<candidates.size(); i++) {
dstCandidates.add(candidates.get(i));
}
return new Candidate(dstCandidates, row, col);
}
}
2 Sudoku : 数独数据结构
/**
* file name : Sudoku.java
* created at : 7:57:10 PM Apr 22, 2015
* created by 970655147
*/
package com.hx.sudoku02;
// 数独类
// 我去, 原来还需要在每一宫都只出现一次,, 我以为我就做完了呢,,, --2015.04.24 10:26
// 现在应该算是 做完了吧, 只不过效率没有 贴吧那家伙的高[现在去看看他的算法吧] 找到第一个结果需要240ms 为什么需要进行深拷贝哪里也不懂, 一直没有找出问题在哪里...?? --2015.04.24 14:38
public class Sudoku {
// 当前计算的数独的数据, 最原始的给定的数据, 每一个方格对应的Candidate
private Integer[][] data;
private Integer[][] backupData;
private Candidate[][] candidates;
// 共有多少个结果, 有多少个方格确定了
private int count;
private int unSetted;
// 方格的个数 每一宫的边长的方格的个数
public final static int GRID_NUM = 81;
public final static int LITTLE_GRID_EDGE = 3;
// 初始化
public Sudoku() {
}
public Sudoku(String data) {
// 初始化data, backupData, candidates
this.data = Tools.parseSudoku(data);
backupData = this.data;
initCandidates(this.data);
unSetted = GRID_NUM;
}
// 解析数独 如果data为null 抛出异常
public void parse() {
if(data == null) {
throw new RuntimeException("data can't be null...");
}
doParse();
}
// 具体的解析数独
// 先获取备选数据最少的方格
// 在依次假设该方格的值为所有的备选数据 更新该方格对应行, 列的所有方格
// 判断是否失败 或者成功 如果是 则恢复之前设置的备选数据为之前的值 [应该是可以优化的]
// 否则继续递归doParse ->
private void doParse() {
// 找到备选数最少的方格 创建一个minSizeCandidate的副本 用于假设该方格的数据, 这样的话不会更改minSizeCandidate
Candidate minSizeCandidate = getMinCandidate(candidates);
Candidate copyMinSizeCandidate = minSizeCandidate.copy();
boolean isEnd = false;
// 遍历所有的备选数据
while(candidateNotNull(copyMinSizeCandidate)) {
// 存储之前的candidates, minSizeCandidate对应的位的数据
Candidate[][] oldCandidates = this.candidates;
Integer oldData = data[minSizeCandidate.getRow()][minSizeCandidate.getCol()];
// 假设minSizeCandidate对应的方格的值, 更新该方格对应的行, 列的所有方格的Candidate
data[minSizeCandidate.getRow()][minSizeCandidate.getCol()] = copyMinSizeCandidate.getCandidate();
// unSetted在updateCandidates中更新
this.candidates = updateCandidates(minSizeCandidate);
// 如果失败了 设置isEnd为true 之后跳出循环 失败的检测需要随时检测
if(isFailed(this.candidates)) {
isEnd = true;
}
// 优化2.
// 如果未设置的方格为为0 才检测是否成功 添加了一个unSetted变量, 但是感觉这里也没有优化多少,,, 大概30ms 1230 -> 1200
if(unSetted == 0) {
Log.log(data);
count ++;
isEnd = true;
}
// 如果没有失败/ 成功, 递归doParse
if(!isEnd) {
doParse();
}
// 恢复minSizeCandidate对应的方格的数据
isEnd = false;
data[minSizeCandidate.getRow()][minSizeCandidate.getCol()] = oldData;
this.candidates = oldCandidates;
}
}
// 判断是否当前数独的假设失败了 判定的条件是假设任意一个方格的不为null的Candidate的备选数据的个数小于1个 则视为失败
// 因为该方格没有备选数据了
private boolean isFailed(Candidate[][] candidates) {
for(int i=0; i<candidates.length; i++) {
Candidate[] row = candidates[i];
for(int j=0; j<candidates.length; j++) {
if(row[j] == null) {
continue;
}
if(row[j].size() < 1) {
return true;
}
}
}
return false;
}
// 更新minSizeCandidate对应的方格对应行, 列 其他方格的Candidate
private Candidate[][] updateCandidates(Candidate minSizeCandidate) {
Candidate[][] updatedCandidates = new Candidate[this.candidates.length][this.candidates.length];
unSetted = GRID_NUM;
// 遍历candidates上minSizeCandidate对应的行或者 列 上的所有方格 [如果该为的数据已经确定 设置该Candidate为null] 更新该方格的Candidate[这里的算法应该可以优化]
// 其他的方格的Candidate不变[浅拷贝]
for(int i=0; i<candidates.length; i++) {
Candidate[] row = candidates[i];
for(int j=0; j<candidates.length; j++) {
// row[j]为空表示该i, j处的数据以确定,
// data[i][j]不为0 表示该位数据以确定[或者被假设]
if(data[i][j] != 0) {
unSetted --;
updatedCandidates[i][j] = null;
// 我去 之前少一个continue....
continue;
}
// 2. 为什么进行深拷贝才能计算出结果...>???? --2015.04.24
// 进行深拷贝是因为 如果不进行深拷贝的话下面的n次递归的过程中 坑定会更改这个Candidate, 这个操作时不可逆的, 因为之前没有备份,
// 那么更改了这个Candidate之后 如果之后因为失败或者成功的原因回溯回去 下一次在访问这个方格的Candidate的时候 他的备选数据 已经变少了 导致忽略了很多方格的备选数据, 最终导致 结果可能计算不出来
// 所以 这里 我才用了一个比较巧妙的方法, 就是在doParse之前将minSizeCandidate备份一下 minSizeCandidate.getCandidate假设数据的时候 就用copyOfMinSizeCandidate 这样更改的就是minSizeCandidate的副本了
// 从而深复制一个minSizeCandidate 而不用深复制minSizeCandidate更新数据是其他行列 并且不在同一个宫格内的方格的Candidate了
// 520ms -> 380ms
if(i==minSizeCandidate.getRow() || j==minSizeCandidate.getCol() || Tools.isInSameGrid(i, j, minSizeCandidate.getRow(), minSizeCandidate.getCol()) ) {
updatedCandidates[i][j] = row[j].updateCandidate(data[minSizeCandidate.getRow()][minSizeCandidate.getCol()]);
} else {
updatedCandidates[i][j] = row[j];
}
}
}
return updatedCandidates;
}
// 判断res是否匹配最开始给定的数据[匹配每一个非待填的数据]
private boolean isResultMatchData(Integer[][] res) {
// 遍历backupData[最开始的data], res 如果两者中任意一个非待填数据不相等 则返回false
for(int i=0; i<backupData.length; i++) {
for(int j=0; j<backupData.length; j++) {
if(backupData[i][j] != 0) {
if(backupData[i][j] != res[i][j]) {
return false;
}
}
}
}
return true;
}
// 根据data初始化 candidates
private void initCandidates(Integer[][] data) {
candidates = new Candidate[data.length][data.length];
// 遍历data 如果该方格数据为0 则获取该方格的Candidate 设置到candidates[i][j]中
// 否则设置candidates[i][j]为null
for(int i=0; i<data.length; i++) {
Integer[] row = data[i];
for(int j=0; j<row.length; j++) {
if(data[i][j] == 0) {
candidates[i][j] = getCandidate(data, i, j);
} else {
candidates[i][j] = null;
}
}
}
}
// 根据data 获取第row行, col列对应的方格的Candidate对象
private Candidate getCandidate(Integer[][] data, int row, int col) {
// 创建candidates对象 并添加0-9到其中
List<Integer> candidates = new ArrayList<Integer>(data.length);
Tools.initAllCandidates(candidates, data.length);
// x表示第几行, y表示第几列
// 移除row行中所有的已经确定的数
for(int i=0; i<data.length; i++) {
if(data[row][i] != 0) {
candidates.remove(data[row][i]);
}
}
// 移除col列中有的已经确定的数
for(int i=0; i<data.length; i++) {
if(data[i][col] != 0) {
candidates.remove(data[i][col]);
}
}
// 移除当前宫中确定的数
Point leftUp = Tools.getLeftUpGridPos(row, col);
Point rightDown = new Point(leftUp.x + LITTLE_GRID_EDGE, leftUp.y + LITTLE_GRID_EDGE);
for(int i=leftUp.x; i<rightDown.x; i++) {
for(int j=leftUp.y; j<rightDown.y; j++) {
if(data[i][j] != 0) {
candidates.remove(data[i][j]);
}
}
}
// 根据candidates, row, col创建Candidate对象
return new Candidate(candidates, row, col);
}
// 获取备选数据最少的方格
private Candidate getMinCandidate(Candidate[][] candidates) {
// 找到第一个存在备选数据的方格
Candidate minSizeCandidate = findFirstCandidate(candidates);
Point p = new Point();
p.x = minSizeCandidate.getRow();
p.y = minSizeCandidate.getCol();
// 遍历所有的之后所有的方格 找到备选数据最少的方格
for(int i=p.x; i<candidates.length; i++) {
Candidate[] row = candidates[i];
for(int j=p.y; j<row.length; j++) {
if(data[i][j]==0 && candidateNotNull(row[j])) {
if(row[j].size() < minSizeCandidate.size()) {
minSizeCandidate = row[j];
}
}
}
}
return minSizeCandidate;
}
// 找到第一个存在备选数据的方格 将其行, 列存储在p中[其实现在没有必要了, 因为candidate中有行, 列的数据] 返回该Candidate
private Candidate findFirstCandidate(Candidate[][] candidates) {
for(int i=0; i<candidates.length; i++) {
Candidate[] row = candidates[i];
for(int j=0; j<row.length; j++) {
if(candidateNotNull(row[j])) {
return row[j];
}
}
}
return null;
}
// 表示该candidate还有备选的数据
private boolean candidateNotNull(Candidate candidate) {
return candidate!=null && candidate.hasCandidates();
}
// 打印当前的数独
public void printSudoku() {
Log.log(data);
}
}
3 Main : 主类, 主要包含了数独的构造的形式, 以及Sudoku的使用方法
/**
* file name : Main.java
* created at : 7:54:31 PM Apr 22, 2015
* created by 970655147
*/
package com.hx.sudoku02;
// Main
public class Main {
// 主要是用于计算找到的第一个解的时间开销
public static long startTime = 0;
public static void main(String []args) {
long start = System.currentTimeMillis();
startTime = start;
String s11_9 = "000000039 000001005 003050800 008090006 070002000 100400000 009080050 020000600 400700000";
String s10_2 = "800000000 003600000 070090200 050007000 000045700 000100030 001000068 008500010 090000400";
String s8_2= "000070009 009800000 070065040 040601070 302000000 005200600 000009800 001400000 600000003";
String s7_4= "006004000 000200005 100900042 021000003 000000097 300000008 000000000 005030070 014826000";
String s6_7= "090004508 000020340 006000002 060003009 030000700 140000000 800010000 370000900 009008030";
String simpleSudoku02 = "000000010 400000000 020000000 000050604 008000300 001090000 300400200 050100000 000807000";
// 构造Sudoku对象 并解析 输出结果, 计算时间
Sudoku sudoku = new Sudoku(s10_2);
// sudoku.printSudoku();
sudoku.parse();
long spent = System.currentTimeMillis() - start;
Log.log("all spent : " + spent + " ms");
// 判断给定的字符串是否符合数独的性质
// String s10_2 = "867453192 413628579 174896253 259317846 386945721 945182637 531274968 728569314 692731485";
// Log.log(Tools.isResultIsSudoku(Tools.parseSudoku(s10_2)));
}
}
5. 运行结果
6. 总结
这里的思路 也是类似于穷举的思路
注 : 因为作者的水平有限,必然可能出现一些bug, 所以请大家指出!