问题
Sudoku Solver Hard
Write a program to solve a Sudoku puzzle by filling the empty cells.
A sudoku solution must satisfy all of the following rules:
- Each of the digits
1-9must occur exactly once in each row. - Each of the digits
1-9must occur exactly once in each column. - Each of the digits
1-9must occur exactly once in each of the 93x3sub-boxes of the grid.
The '.' character indicates empty cells.
Example 1:
Input: board = [
["5","3",".",".","7",".",".",".","."],
["6",".",".","1","9","5",".",".","."],
[".","9","8",".",".",".",".","6","."],
["8",".",".",".","6",".",".",".","3"],
["4",".",".","8",".","3",".",".","1"],
["7",".",".",".","2",".",".",".","6"],
[".","6",".",".",".",".","2","8","."],
[".",".",".","4","1","9",".",".","5"],
[".",".",".",".","8",".",".","7","9"]
]
Output: [
["5","3","4","6","7","8","9","1","2"],
["6","7","2","1","9","5","3","4","8"],
["1","9","8","3","4","2","5","6","7"],
["8","5","9","7","6","1","4","2","3"],
["4","2","6","8","5","3","7","9","1"],
["7","1","3","9","2","4","8","5","6"],
["9","6","1","5","3","7","2","8","4"],
["2","8","7","4","1","9","6","3","5"],
["3","4","5","2","8","6","1","7","9"]
]
Explanation: The input board is shown above and the only valid solution is shown below:
Constraints:
board.length == 9board[i].length == 9board[i][j]is a digit or'.'.- It is guaranteed that the input board has only one solution.
解题思路一
如果有喜欢玩数独的朋友,看到这个题肯定跟我一样很开心,以为之前玩数独的经验可以有用武之地了。不过当我做完才发现,跟我想象的不一样,根本没有用武之地-_-|||。计算机擅长的是穷举,程序员的工作就是让它聪明地穷举。废话不多说,回到本题,本文将给出两种解法,一种是穷举,另一种是聪明的穷举。
做过数独的朋友应该知道,开始做题时,一个空格往往有多个可能的候选数字,我们需要分析各个节点之间的制约关系,除掉一些候选数字,直到每个空格都只剩一个候选项,才能得到最终的解。
解法一的思路简单明了:
- 循环直到遇到空格
'.' - 尝试所有候选数字,如果能找到一个,则将其填入数独表,继续寻找下一个空格
'.',重复此步骤 - 如果没有候选数字,则返回到上一个空格,并将其重置为
'.',在上一格继续执行步骤2,尝试其他可能的候选数字 - 若在步骤2中处理完所有空格,则数独解答成功
其实就是在来来回回不断地尝试。用递归的方式来表达,需要定义递归函数doSolve(char[][] board, int row, int col),来处理某个节点。从board[0][0]开始,按照从左至右,从上至下的顺序,对board中的元素逐个调用递归函数。当遇到上述步骤3中的情况,无法继续尝试下去时,递归函数doSolve返回失败。如果可以继续尝试,则返回成功。当所有空格都被递归函数访问完毕,则解答成功。
解法一虽然被LeetCode接受,但是执行比较慢,仅仅打败了14%的竞争方案。
参考答案一
public class Solution {
public void solveSudoku(char[][] board) {
doSolve(board, 0, 0);
}
private boolean doSolve(char[][] board, int row, int col) {
// if all cells have been solved, then return true
if(row == 9 && col == 0) {
return true;
}
// decide the next row & col to solve
final int nextRow = (col + 1) == 9 ? row + 1 : row;
final int nextCol = (col + 1) == 9 ? 0 : col + 1;
// if current cell has been solved then solve the next one
if(board[row][col] != '.') {
return doSolve(board, nextRow, nextCol);
}
// if current cell hasn't been solved, then try from 1 to 9
for(char num = '1'; num <= '9'; num++) {
if(isValid(board, row, col, num)){
// if the candidate num is valid
// set it to board temporarily
board[row][col] = num;
// then solve the next one
if(doSolve(board, nextRow, nextCol)) {
// if all the rest cells have been solved, return true
return true;
}
// if any rest cells can't be solved, set back to '.' and continue trying
board[row][col] = '.';
}
}
return false;
}
private boolean isValid(char[][] board, int row, int col, char num) {
// the row & col of the first cell in the cube
int cubeRow = (row / 3) * 3;
int cubeCol = (col / 3) * 3;
for (int i = 0; i < 9; i++) {
// if num already exists in the same col
// or num already exists in the same row
// or num already exists in the same cube
if (board[i][col] == num
|| board[row][i] == num
|| board[cubeRow + i / 3][cubeCol + i % 3] == num) {
return false;
}
}
return true;
}
}
解题思路二
这个解法也有人称之为李显龙算法,是的!你没有看错,就是那个李显龙,新加坡现任总理,前总理李光耀的儿子。他当年在剑桥大学数学系求学时,用C语言写出了这个算法。这位李先生可能是总理里面最会写程序的,程序员里面最会当总理的;P。
在解读这个算法之前,我们先来好好复习一下位运算吧。下面这些操作符还记得吗?
&:按位与, e.g. 010 & 110 = 010|:按位或, e.g. 010 | 110 = 110~:按位取反, e.g. ~010 = 101&=:按位与分配, e.g. a = 11110, b = 1000, 执行a &= ~b后, a = 10110|=:按位或分配, e.g. a = 10110, b = 1000, 执行a |= b后, a = 11110<<:移位, e.g. 1 << 4 = 10000-:负转换, Java中表示为补码,即反码+1, e.g. -10110 = 01001 + 1 = 01010
回到这个算法,它究竟好在哪里呢?其实它的总体思想跟解法一差不多,都是穷举,只不过用位运算对其中的一些操作进行了优化。具体的优化如下:
九宫格中数字的占用情况
解法一中利用原始的board[][]来记录数字的占用情况。
解法二用Bitmap的方式,表示一行,一列或者一宫中已经被占用的数字。比如int[9] rowBitmap, colBitmap, cubeBitmap;,Bitmap中包含10个bit,最低位(最右边的bit)永远是0,而剩下的bit从低位到高位分别表示数字1到9是否可用,1表示对应的数字可用,而0表示已经被占用。比如0b1000000010表示对应的行、列或者宫中,1和9没有被占用。
另外,Bitmap还被用来表示一个空格中的备选数字,比如int cellBitmap的值为0b1000000010时,表示1和9没有在该空格所在的行、列或者宫中出现。
寻找空格中的候选数字
解法一中寻找一个空格的候选数字时,使用了一层for循环,寻找对应行、列,宫中未使用的数字。
解法二就简单了,cellBitmap = rowBitmap[row] & colBitmap[col] & cubeBitmap[cube];一次位运算拿走不谢。这就是李显龙算法的精妙之处!
处理优先级
解法一中并没有设置优先顺序,而是按照空格出现的自然顺序进行处理。
解法二由简入繁,从候选数字最少的开始处理,减少了尝试失败时,可能需要返回的级数。为了构建优先级,要将二维的九宫格int[9][9] board转换为一维数组int[81] entry。
同时,用int[81] sequence表示处理的优先顺序,它的值对应了entry中的一维坐标。board中已知的数字放在最前面,然后处理空格,从cellBitmap中1最少的空格开始处理。我们可以使用Java提供的函数Integer.bitCount (cellBitmap)来方便地获取1的数量。
坐标转换
为了避免重复进行一维坐标到二维坐标的转换,使用int[81] squareToRow, squareToCol, squareToCube;保存坐标映射,避免重复计算。
其他位运算的使用
board与entry之间的转换
为了方便位运算,entry中是用Bitmap方式表示数字,即board[i][j]==3表示为entry[i*9+j]==0b1000。将3转换成0b1000只需要用位移操作,1 << board[i][j]
,而将0b1000转换成3,可以是用Java提供的函数Integer.numberOfTrailingZeros(entry[i*9+j])。
修改或还原Bitmap
在尝试过程中,需要修改行、列、宫对应的Bitmap,如果尝试失败,还需要还原成原来的Bitmap。修改Bitmap时使用按位与分配rowBitmap[rowIdx] &= ~nextDigitBit,还原时使用按位或分配rowBitmap[rowIdx] |= nextDigitBit。
例如:
rowBitmap[rowIdx] = 1111111110, nextDigitBit = 1000, 执行rowBitmap[rowIdx] &= ~nextDigitBit修改之后,rowBitmap[rowIdx] == 1111110110;然后再执行rowBitmap[rowIdx] |= nextDigitBit还原之后,rowBitmap[rowIdx] == 1111111110。
获取候选数字
当我们知道了一个空格中所有的备选数字cellBitmap,如何逐个取出来使用呢?这里需要使用负转换和按位与。执行nextDigitBit = cellBitmap & -cellBitmap可以得到最低位的1和它后面的0。比如cellBitmap = 1001000000,执行上述运算后nextDigitBit == 1000000,是不是很神奇!然后修改cellBitmap &= ~nextDigitBit,将对应的Bit置零,循环尝试所有的候选数字,直到cellBitmap == 0。
思路分析就到这里,该解法最终打败了99.69%的竞争方案,李显龙总理请收下我的膝盖Orz。
参考答案二
public class Solution {
static final int ALL_ZEROS = 0;
// 0x3fe = 1111111110
static final int ALL_ONES = 0x3fe;
// bitmaps for row/col/cube, 1 means available
int[] rowBitmap, colBitmap, cubeBitmap;
// 1D array to store board nums' pos in bitmap, e.g. board[i][j] == 3, then entry[i*9+j] == 1000
int[] entry;
// 1D array to store the priority of SQUARE index, 0 <= sequence[i] < 81, 0 <= i < 81
int[] sequence;
// always points to the first empty cell's SQUARE index, which is stored in SEQUENCE
int seqStart;
// 1D utility arrays to store mapping from SQUARE to ROW/COLs/CUBES
int[] squareToRow, squareToCol, squareToCube;
public void solveSudoku(char[][] board) {
seqStart = 0;
rowBitmap = new int[9];
colBitmap = new int[9];
cubeBitmap = new int[9];
entry = new int[81];
sequence = new int[81];
squareToRow = new int[81];
squareToCol = new int[81];
squareToCube = new int[81];
// initialize all helping data structures
// all digits are initially all available (marked by 1) in all rows/columns/cubes
for (int i = 0; i < 9; i++) {
rowBitmap[i] = colBitmap[i] = cubeBitmap[i] = ALL_ONES;
}
// sequence stores all SQUARE indices of all cells, with 0..start-1 being all filled cells
// and the rest all empty, initially start = 0
for (int i = 0; i < 81; i++) {
sequence[i] = i;
}
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
// mapping from SQUARE to I/J is also beneficial: avoid calculating I/J from SQUARE later
int square = i * 9 + j;
squareToRow[square] = i;
squareToCol[square] = j;
squareToCube[square] = (i / 3) * 3 + j / 3;
}
}
// fill in the given cells. update the bitmaps at the same time
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] != '.') {
int square = i * 9 + j, val = board[i][j] - '0';
// e.g. val = 3, valbit = '1000'
int valbit = 1 << val;
// e.g. bitmap = 1111111110, valbit = 1000, bitmap &= ~valbit makes 1111110110
rowBitmap[i] &= ~valbit;
colBitmap[j] &= ~valbit;
cubeBitmap[(i / 3) * 3 + j / 3] &= ~valbit;
entry[square] = valbit;
int seqIter = seqStart;
// compact non-empty cells to the front
// and use seqStart to mark the first empty cell's position
while (seqIter < 81 && sequence[seqIter] != square) {
seqIter++;
}
swapSeq(seqStart++, seqIter);
}
}
}
// main solver process
boolean success = place (seqStart);
assert success : "Unsolvable Puzzle!";
// dump result back from ENTRY array to BOARD
for (int s = 0; s < 81; s++) {
int i = squareToRow[s], j = squareToCol[s];
board[i][j] = (char) (Integer.numberOfTrailingZeros (entry[s]) + '0');
}
}
boolean place (int seqPos) {
// if all cells have been solved, then return true
if (seqPos >= 81) {
return true;
}
// find the most determinable cell and swap to the front of SEQUENCE
int seqNext = nextPos (seqPos);
swapSeq (seqPos, seqNext);
int square = sequence[seqPos];
int rowIdx = squareToRow[square];
int colIdx = squareToCol[square];
int cubeIdx = squareToCube[square];
int cellBitmap = rowBitmap[rowIdx] & colBitmap[colIdx] & cubeBitmap[cubeIdx];
while (cellBitmap > 0) {
// get each available bit/digit in order
int nextDigitBit = cellBitmap & -cellBitmap;
cellBitmap &= ~nextDigitBit;
entry[square] = nextDigitBit;
// claim this DIGIT is used in row/column/cube
rowBitmap[rowIdx] &= ~nextDigitBit;
colBitmap[colIdx] &= ~nextDigitBit;
cubeBitmap[cubeIdx] &= ~nextDigitBit;
if (place (seqPos + 1)) {
return true;
}
// undo claims in the bitmaps
rowBitmap[rowIdx] |= nextDigitBit;
colBitmap[colIdx] |= nextDigitBit;
cubeBitmap[cubeIdx] |= nextDigitBit;
entry[square] = ALL_ZEROS;
}
swapSeq (seqPos, seqNext);
return false;
}
// find among empty cells the most determinable one: least bits on its bitmap;
int nextPos (int pos) {
int minIdx = pos, minDigitCount = 100;
for (int i = pos; i < 81; i++) {
int square = sequence[i];
// number of bits in cellBitmap is the number of digits that this cell can take
int cellBitmap = rowBitmap[squareToRow[square]]
& colBitmap[squareToCol[square]]
& cubeBitmap[squareToCube[square]];
// counts the '1's, so you know how many digits this CELL can take: we want the minimum one
int numPossibleDigits = Integer.bitCount (cellBitmap);
if (numPossibleDigits < minDigitCount) {
minIdx = i;
minDigitCount = numPossibleDigits;
}
}
return minIdx;
}
void swapSeq (int i, int j) {
int tmp = sequence[i];
sequence[i] = sequence[j];
sequence[j] = tmp;
}
}
拓展训练
来放松一下,做一个简单点的有效数独问题吧!
或者到作者的LeetCode专栏中看看,有没有其他感兴趣的问题吧!
资料链接
原著李显龙脸书上的解法 www.facebook.com/leehsienloo…