LeetCode 37 Sudoku Solver
方法1:普通DFS
想法:这个题反正填数独肯定是暴搜来填的,但是就像众多的DFS题目一样,每个题有每个题的写法。第一种写法是我之前写的一个比较基本的做法,思路就是沿着行列扫描,反正就是扫到一个没有值的地方,也就是原矩阵当中带点的地方,我们就试图在这个地方放一个数,从1试到9。如何判断可以放某个数在这个地方呢?我用了最愚蠢的办法,就是暴力扫描这一行,扫描这一列,扫描这个九宫格,看看有没有一样的元素。这个地方肯定是可以提升的,但是因为我当时在写的时候觉得反正是9*9的大方格,暴力扫描这些东西的时间复杂度原则上来说还是常数,所以我就先没优化,先交交看。那么,按照上述所说,就直接DFS就可以了,DFS的时候带上坐标,按照从左到右,从上到下的顺序来。因为我们这道题题目说给的数独一定有且只有一个解,因此我们用boolean型DFS,搜到一个可行的方案之后,直接return结束所有的搜索。
代码:
class Solution {
public void solveSudoku(char[][] board) {
canSolve(board, 0, 0);
}
private boolean canSolve(char[][] board, int x, int y) {
if (x == 9) {
return true;
}
int[] next = getNext(x, y);
int nx = next[0], ny = next[1];
if (board[x][y] != '.') {
return canSolve(board, nx, ny);
}
for (char c = '1'; c <= '9'; c++) {
if (isValid(board, x, y, c)) {
board[x][y] = c;
if (canSolve(board, nx, ny)) {
return true;
}
board[x][y] = '.';
}
}
return false;
}
private int[] getNext(int x, int y) {
y++;
if (y == 9) {
x++;
y = 0;
}
return new int[] {x, y};
}
private boolean isValid(char[][] board, int x, int y, char c) {
for (int i = 0; i < 9; i++) {
if (board[i][y] == c) {
return false;
}
}
for (int j = 0; j < 9; j++) {
if (board[x][j] == c) {
return false;
}
}
int up = 3 * (x / 3), left = 3 * (y / 3);
int down = up + 2, right = left + 2;
for (int i = up; i <= down; i++) {
for (int j = left; j <= right; j++) {
if (board[i][j] == c) {
return false;
}
}
}
return true;
}
}
方法2:DFS + 剪枝
想法:对于DFS,如果多写一些题目,搞清楚搜索的顺序的话,那么写出一个正确的解法是没有问题的,但常常并不能保证这个搜索的效率有多高,所以剪枝就成为了DFS当中的一项艺术。对于这道题,我是从y总的视频里www.acwing.com/video/61/ 学来的这种方法,之后在LeetCode又提交了一下,直接beat 99.42%的人。
这个题剪枝的想法我感觉是来自于人类填数独的经验。就比方说拿到一道数独的题目你会怎么填?反正我肯定会先填决策可能性少的那些格子,比方说我拿来一看,某个九宫格里就三个数没填过了,比方说是1和2和3,然后继续观察它们所在的行和列,又排除了一些可能性,比方说最后导致于某个格子,它只能填1,因为它所在的这一行出现了2,它所在的这一列出现了3,这样的话我肯定是先把这个元素填上。我们先去填那种决策可能性少的格子,会使得填数独的效率大大提升。
那么怎么去实现这一点呢?这题采用的方法是位运算,就是用一个二进制9位的数字来代表状态。1代表可以选,0代表不能选,比方说101001000,它代表的就是我只能填4或7或9。然后,假设说有一个row数组,代表每一行的状态,col数组代表每一列的状态,cell[][]数组代表这个所在的九宫格的状态,那么,三个状态直接”与“,出来的数就代表三方卡完(集合交集)之后,还能填的是什么。所以我每次填一个数字,我都去从所有格子里面找出那个决策可能性最少的格子去填。这样思考就会发现,除此之外,我们需要知道对于一个代表状态的数字num,我们想快速地知道它里面有多少个1。这里可以使用lowbit运算,但挨个计算还是有点慢,因此干脆先全算完,因为最大的状态也无非就是111111111,是2^9 - 1,因此干脆全算完存到数组ones[]里。除此之外,我们DFS的时候,会试图在遍历到的这个位置上放一个数字,比方说这个位置,算出来代表可以放的元素的那个状态为100100100,会发现可能可以放3、6、9,每次会用lowbit函数找到最低位的1,但是,给一个lowbit,我们怎么快速的知道它代表的可以放的数字是多少呢?比方说二进制100,我们知道它代表可以放3,但怎么快速查找?可以仍然先开一个数组map[],然后把lowbit对应的放哪个数字先存进去。
综上,思路大概就有了。因为这道题我们采用这样的策略来找下一个试图放数字的位置,那么我们就不能再用位置坐标为顺序遍历了,我们需要记录当前还需要填几个格子,然后带着这个cnt数字递归。
代码:
class Solution {
private int N = 9;
private int[] row = new int[N];
private int[] col = new int[N];
private int[][] cell = new int[3][3];
private int[] map = new int[1 << N];
private int[] ones = new int[1 << N];
public void solveSudoku(char[][] board) {
for (int i = 0; i < N; i++) map[1 << i] = i;
for (int i = 0; i < (1 << N); i++) {
int s = 0;
for (int j = i; j > 0; j -= lowbit(j)) {
s++;
}
ones[i] = s;
}
for (int i = 0; i < N; i++) {
row[i] = (1 << N) - 1;
col[i] = (1 << N) - 1;
}
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
cell[i][j] = (1 << N) - 1;
}
}
int cnt = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (board[i][j] == '.') {
cnt++;
}
else {
int t = board[i][j] - '1';
row[i] -= (1 << t);
col[j] -= (1 << t);
cell[i / 3][j / 3] -= (1 << t);
}
}
}
dfs(cnt, board);
}
private boolean dfs(int cnt, char[][] board) {
if (cnt == 0) {
return true;
}
int minv = 10;
int x = 0, y = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (board[i][j] == '.') {
int tmp = ones[get(i, j)];
if (tmp < minv) {
minv = tmp;
x = i;
y = j;
}
}
}
}
for (int i = get(x, y); i > 0; i -= lowbit(i)) {
int t = map[lowbit(i)];
row[x] -= (1 << t);
col[y] -= (1 << t);
cell[x / 3][y / 3] -= (1 << t);
board[x][y] = (char) ('1' + t);
if (dfs(cnt - 1, board)) {
return true;
}
row[x] += (1 << t);
col[y] += (1 << t);
cell[x / 3][y / 3] += (1 << t);
board[x][y] = '.';
}
return false;
}
private int lowbit(int x) {
return x & (-x);
}
private int get(int x, int y) {
return row[x] & col[y] & cell[x / 3][y / 3];
}
}