在生成数独题目时,流程是:生成一个完整的数独——随机挖空——解题——如果只有一个解,则生成该题目,如果有多个解,则重新挖空。
数独题目的生成涉及的一个算法是回溯法,这里面的完整数独的生成、求解都用到了回溯法。
1. 生成完整数独
在生成部分,先生成所有宫的1,再生成所有宫的2,以此类推。
主要有三个值:
boolean[][][] placeable = new boolean[9][9][9];// 第几个数字,第几宫,第几号位置
int[][] stepPos = new int[9][9];// 第几个数字,第几宫,值是位置
int[][] result=new int[9][9];
placeable 用来存储每个数字在每个宫的每个位置是否可以放置,初始时,只要该位置没有被占就可以放置,在生成数独的过程中,尝试在这个位置放置某数,如果与行列宫有冲突,则需更新placeable 。
stepPos 用来存储某个宫中已经放置的数字的位置。
result既是存储生成的数独。
生成阶段的回溯是通过一个双重循环实现的:
private static int[][] tryGenerateFinalAnswer() {
int[][] result=new int[9][9];
for (int number = 0; number < 9; number++) {
//注意这里没有gong++
for (int gong = 0; gong < 9;) {
ArrayList<Integer> PlaceableList = getPlaceableList(number, gong);
int length = PlaceableList.size();
//回溯算法的重点就是这里,如果当前一步走不下去,就返回上一步,重走
if (length <= 0) {
resetPlaceable(result,number, gong);
gong--;
if (gong < 0) {
number--;
gong = 8;
}
removeNum(result, number, gong);
}else {
int pos = PlaceableList.get(random.nextInt(length));
if (isCollide(result,number, gong, pos)) {
placeable[number][gong][pos] = false;
}else {
addNum(result, number, gong, pos);
gong++;
}
}
}
}
return result;
}
其实一开始写数独生成的时候,想的是先随机生成第一个宫的所有数字,第二个宫在不与第一个宫冲突的情况下生成所有数字,以此类推,就是以宫为主顺序生成数独。这样的做法理论上可行,但实际上需要非常大量的运算,而且极易容易冲突。因为后面一旦走不下去,往往要返回一个宫的所有数字,但是以数字为主顺序,走不下去就只需要返回一个宫的一个数字。
2. 随机挖空
随机挖空的部分比较简单,但是要注意使用另一个二维数组来存储生成的题目,不要在完整的数组上动,因为后面解题部分要对比完整的数组,看看解题是否正确。
3. 解题
解题部分时参考了另一位大佬的代码,之前看的时候距今已经过了一年有余,所以找不到他的博客了。
解题部分的思想非常的巧妙,首先将数独题目中1-9这9个数,转化成二进制中1的位置。000000100表示3,如001000000表示7,而题目中空缺的位置,则以111111111表示,这表示这个位置的候选数字是1~9。
然后根据已确定的数字分析更新空格的候选数字,比如候选值有2、3、6,则该空的二进制数就是000100110。当二进制中只有一个1,则表示该值可以确定,使用Integer.bitCount(data[m][i]) 方法判断二进制中有几个1。
这里的分析主要就是排除行列宫中已有的数字。
一直循环分析所有空格,直到不能从已确定的数字中分析出新的候选信息,就从一个空格的候选值中假设一个值填进格子中,再以此推测剩余的空格,直到所有格子都被填满,或者是填补下去,那么就返回之前的假设,填其他值,以此类推。这其实和我们正常解数独的思路是一样的。
如果题目已经求得两个解,则返回,重新生成题目。因为这个题目是从一个完整的数独挖空而来,所以不可能没有解。
解题的回溯部分主要通过递归实现:
private static void solve(int[][] data) {
if(resultNum>1) {
return;
}
analyse(data);
int result = check(data);
if (result == 1) {
int[] position = findLessCandidatesPos(data);
int pv = data[position[0]][position[1]];
int pvcount = Integer.bitCount(pv);
for (int i = 0; i < pvcount; i++) {
int testV = 1 << ((int) (Math.log(Integer.highestOneBit(pv)) / Math.log(2)));
pv ^= testV;
int[][] copy = copyArray(data);
copy[position[0]][position[1]] = testV;
//这里不理解为什么要返回
if(i>1) {
return;
}
solve(copy);
}
}else if (result == 0) {
resultNum++;
System.out.println("------------------------------------第"+(resultNum)+"个答案---------------------"
+ (System.nanoTime() - startTime) / 1000000.0 + "ms---");
answer=data;
binaryToInt(answer);
printByRow(answer);
}
}
解题中值得借鉴的就是将数字1~9表示成二进制中1的位置,这样可以很好的表示候选值。除了数独中的数字与候选值表示成二进制中1的位置,
在找候选值的方法中,也是用三个二进制数分别表示当前值所在的行、列、宫已经存在的数字,来计算候选值的。
在判断是否冲突的方法,也是用三个二进制数分别表示当前值所在的行、列、宫已经存在的数字,来判断是否冲突的。
唯一解的数独题目生成器代码://download.csdn.net/download/Michaelia_hu/12013857