数独的基本规则我想大家也都知道,不知道的可以先去百度百科看一下数独的规则。
位运算对于学过离散数学的人应该很熟悉,但是对于没有学过位运算的人可能就会觉得很头疼,而我就属于后者。但是位运算其实也没有很复杂,我在这里也就不再多赘述了,不懂的朋友还是去百度百科上看一下。
对于如下的数独,0表示需要你填的数。
6 1 0 2 7 0 5 0 9
8 9 5 6 3 1 7 2 4
0 4 0 5 8 0 0 1 0
9 0 8 3 4 0 1 0 0
0 3 6 0 9 7 2 4 0
0 7 2 1 5 6 0 3 8
3 0 4 7 1 5 0 9 0
5 2 1 9 6 0 4 7 0
7 6 9 4 2 0 8 5 0
填好之后应该是这样子的
6 1 3 2 7 4 5 8 9
8 9 5 6 3 1 7 2 4
2 4 7 5 8 9 3 1 6
9 5 8 3 4 2 1 6 7
1 3 6 8 9 7 2 4 5
4 7 2 1 5 6 9 3 8
3 8 4 7 1 5 6 9 2
5 2 1 9 6 8 4 7 3
7 6 9 4 2 3 8 5 1
数独的设计一般是先回溯生成一个完整的数独,然后再挖空某一些数字。一般情况下,解是唯一的,当然也不排除挖空太多导致有多种解(极端情况下是挖空所有数字)。
那如何求解呢?暴力枚举可以吗?肯定可以,数字都不大,是可以在有限的时间完成的,但有没有更快的方法呢?当然是回溯法。其实回溯法也是一种枚举,不过是有返回的枚举。回溯法最核心的就在于回溯这两个字上,如何回溯?下面我们在实际代码中来看。加粗和斜体的部分就是有关位运算的操作,位运算只是辅助工具,核心还是回溯法。
import java.util.Scanner;
public class shudu {
static int count = 0;//记录需要填写的数独的个数
static int[][] r = new int[81][2];//需要填写的数独的坐标集合
static int upperlim = (int)Math.pow(2, 9) - 1;//本算法中二进制的上界即0b111111111(0b表示二进制,后面有9个1)
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner scan = new Scanner(System.in);
int[][] a = new int[9][9];//数独
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
a[i][j] = scan.nextInt();
}
}
scan.close();
int[] row = new int[9];//对于行已经存在的数的二进制
int[] col = new int[9];//对于已经存在的数的二进制
int[][] p = new int[3][3];//对于9宫格已经存在的数的二进制
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (a[i][j] == 0) {
r[count][0] = i;//需要填的数字的横坐标
r[count][1] = j;//需要填的数字的纵坐标
count++;//个数
continue;
}
row[i] += (int)Math.pow(2, a[i][j]-1);//第i行中存在数字j的话就让row【i】自增2^(a[i][j]-1),比如上例中第0行就是row[i] = 0b101110011,表示已经存在1,2,5,6,7,9了。下同
col[j] += (int)Math.pow(2, a[i][j]-1);
p[i/3][j/3] += (int)Math.pow(2, a[i][j]-1);
}
}
backtrack(a, row, col, p, 0);
}
public static void backtrack(int[][] a, int[] row, int[] col, int[][] p, int b) {
if (b == count) {
display(a);
return;
}
int pos = upperlim & ~(row[r[b][0]] | col[r[b][1]] | p[r[b][0]/3][r[b][1]/3]);
//r[b][0]和r[b][1]分别表示第b个需要填的数字的横坐标和纵坐标。row|col|p表示该空格处所处的行,列,与9宫格已经存在的数字的并,也就是说该空格不可以填的数字,那么upperlim & ~(row|col|p)就表示该空格可以填的数字集合。即同一层级的所有可行解。
while (pos != 0) { //这个循环表示同一层级的多种选择
int u = pos & (-pos);//这个位运算的操作有点复杂,其意义是取出一个可行的选择。
pos -= u;//从所有可行选择中减掉取出的选择
a[r[b][0]][r[b][1]] = (int)(Math.log(u)/Math.log(2)) + 1;//这行和下面4行表示将取出的可行解作为当前解,并改变相关量
row[r[b][0]] += u;
col[r[b][1]] += u;
p[r[b][0]/3][r[b][1]/3] += u;
backtrack(a, row, col, p, b+1);//进入下一层递归
a[r[b][0]][r[b][1]] = 0;//这行和下面4行就是在回溯,即回到原状态,便于同一层级的下一种选择进行操作
row[r[b][0]] -= u;
col[r[b][1]] -= u;
p[r[b][0]/3][r[b][1]/3] -= u;
}
}
public static void display(int[][] a) {
//打印二维数组的方法
for (int i = 0; i < a.length; i++) {
for (int j = 0; j < a[0].length; j++) {
System.out.print(a[i][j]+" ");
}
System.out.println();
}
}
}
好啦,自动数独程序就完成啦,是不是发现数独,回溯法以及位运算都没有那么神秘了呢?