js实现解数独游戏

983 阅读3分钟

这是我参与更文挑战的第4天,活动详情查看: 更文挑战

编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 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] 是一位数字或者 '.'
  • 题目数据 保证 输入数独仅有一个解

解法

递归 + 回溯 + 位运算

递归

sudoku.png

  • 每次选取可填数字最少的空格,试探次数更少,发现错误更快。

位运算

  • 使用 9-bit 保存数字 1~9 的占用情况,通过位运算处理,更加轻松高效。

250px-Sudoku-by-L2G-20050714.png

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;
    }
}