这是我参与更文挑战的第4天,活动详情查看: 更文挑战
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
- 数字
1-9
在每一行只能出现一次。 - 数字
1-9
在每一列只能出现一次。 - 数字
1-9
在每一个以粗实线分隔的3x3
宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 '.'
表示。
示例:
输入: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"]] 输出:[["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"]] 解释:输入的数独如上图所示,唯一有效的解决方案如下所示:
提示:
board.length == 9
board[i].length == 9
board[i][j]
是一位数字或者'.'
- 题目数据 保证 输入数独仅有一个解
解法
递归 + 回溯 + 位运算
递归
- 每次选取可填数字最少的空格,试探次数更少,发现错误更快。
位运算
- 使用 9-bit 保存数字 1~9 的占用情况,通过位运算处理,更加轻松高效。
9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | ||
---|---|---|---|---|---|---|---|---|---|---|
行 | r = rows[5] | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 0 |
列 | c = columns[3] | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 1 |
3x3方格 | b = boxs[1][1] | 0 | 1 | 0 | 1 | 0 | 0 | 1 | 1 | 0 |
或运算 | x = r | c | b | 0 | 1 | 1 | 1 | 0 | 1 | 1 | 1 | 1 |
取反 9-bit | p = x ^ 0b111111111 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
截取最低位 1 | s = -p & p | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
清除截取的位 | p ^ s | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
代码
/**
* @param {character[][]} board
* @return {void} Do not return anything, modify board in-place instead.
*/
var solveSudoku = function (board) {
new Sudoku(board).solve();
};
class Sudoku {
constructor(board) {
this.board = board;
//行
this.rows = new Array(9).fill(0);
//列
this.columns = new Array(9).fill(0);
//3x3方格
this.boxs = Array.from({ length: 3 }, () => new Array(3).fill(0));
//未填空格
this.emptyCells = new Set();
}
solve() {
//初始化已知的数字
for (let i = 0; i < 9; i++) {
for (let j = 0; j < 9; j++) {
let num = this.board[i][j];
if (num !== ".") {
//将数字转化为二进制标记
//1 -> 0b1, 2 -> 0b10, 3 -> 0b100, 4 -> 0b1000 ...
const sign = 1 << (Number(num) - 1);
this.rows[i] |= sign;
this.columns[j] |= sign;
this.boxs[Math.floor(i / 3)][Math.floor(j / 3)] |= sign;
} else {
this.emptyCells.add((i << 4) | j);
}
}
}
//主逻辑
return this.fillNext();
}
fillNext() {
let cellInfo = this.getEmptyCell();
if (cellInfo === null) {
//没有空格,解题成功
return true;
}
let [i, j, possible] = cellInfo;
while (possible) {
//截取其中一个可能性
const sign = -possible & possible;
//填入空格
this.fillCell(i, j, sign);
//继续下一个填充
if (this.fillNext()) {
//填充成功
return true;
} else {
//排除当前数字
possible ^= sign;
//清空空格
this.cleanCell(i, j, sign);
}
}
//穷尽所有可能性,回溯
return false;
}
getEmptyCell() {
let min = 10;
let cellInfo = null;
for (const id of this.emptyCells) {
const i = id >> 4, j = id & 0b1111;
const possible = this.getCellPossible(i, j);
const count = this.countPossible(possible);
if (min > count) {
//挑选可能性最少的格子,理论上可减少犯错回溯
cellInfo = [i, j, possible];
min = count;
}
}
return cellInfo;
}
countPossible(possible) {
//计算二进制 1 的数量
let count = 0;
while (possible) {
possible &= (possible - 1);
count++;
}
return count;
}
fillCell(i, j, sign) {
//对应位变成1,标记占用
this.rows[i] |= sign;
this.columns[j] |= sign;
this.boxs[Math.floor(i / 3)][Math.floor(j / 3)] |= sign;
//填入空格
this.emptyCells.delete((i << 4) | j);
this.board[i][j] = String(Math.log2(sign) + 1);
}
cleanCell(i, j, sign) {
//对应位变为0,清除占用
this.rows[i] &= ~sign;
this.columns[j] &= ~sign;
this.boxs[Math.floor(i / 3)][Math.floor(j / 3)] &= ~sign;
//清空格子
this.emptyCells.add((i << 4) | j)
this.board[i][j] = ".";
}
getCellPossible(i, j) {
//获取格子可能的取值,二进制1表示可选
return (this.rows[i] | this.columns[j] | this.boxs[Math.floor(i / 3)][Math.floor(j / 3)]) ^ 0b111111111;
}
}