leetCode 37 解数独 Soduko Solver
以前比较喜欢玩数独,一直想尝试一下编程解数独,练习算法时看到LeetCode上正好有这样一题,于是决定做一下。做出来的过程中确实花费了一番功夫,遂记录下来整个过程。
整理思路
开始时一头雾水,于是回想自己做数独的逻辑与策略:
1. 有没有哪一个位置开始就可以确定值的?把能确定值的空格全部填上
2. 当剩余的空格为多解时,找解最少的开始试
解题方法
(0,0)
开始遍历数独,当坐标(x, y)==='.'
时,获取(x,y)
位置的可能取值集合rest
rest.length !== 0
,将(x,y)
位置的值替换成rest[0]
,遍历继续rest.length === 0
则开始回溯- 遍历完
(8,8)
,输出结果
由上可以总结出需要写的一些方法
获取当前位置可能值的集合
给定(x,y)
及数独,获取数独(x,y)
位置的可能取值集合rest
本示例中这个获取可能的值方法是根据LeetCode 36来的,还可以优化。根据人实际决策时,人解数独开始时用了排除法。比如,数字8在某个小九宫格中,不可能出现一些位置(因为这些位置所在的行列中包含了数字8),然后确定数字8的位置。这一条逻辑未加入到代码之中。
继续遍历
判断当前搜索位置(x,y)
处的rest
1. rest.length === 0
,需要回溯
2. rest.length !== 0
,遍历继续
回溯
简单的说就是退到上一个填入值的位置,如果这个位置除了已经用的值外还有其他可能的值,则填入;如果没有了则继续回溯
如何知道已经用了哪些值?
返回可能值的时候将数组排序,当前值知道,则当前值的下个index
也就知道了
如何找到上次的修改位置?
数组中原始数字为字符串,填入数字为Number
即可区分
其他注意点 :回溯时要把当前位置的值还原成
'.'
否则获取当前位置的rest
时会得到错误的结果
代码
/**
* @param {arr[][]} board
* @return {void} Do not return anything, modify board in-place instead.
*/
var solveSudoku = function (board) {
search(board, 0, 0);
function search(board, x, y) {
let temp;
// 主体遍历数独
while (x !== 9 || y !== 0) {
if (board[x][y] !== '.') { //跳过不需要填数字的部分
y === 8 ? (x += 1, y = 0) : y += 1;
} else {
// 决策需要填的数字
temp = getPossibleValue(board, x, y);
if (!temp.length) {
// 没有可以填的数字了 ->> 需要回溯
console.log(x, y, 'temp.length === 0,开始回滚')
let coordinate = rollBack(board, x, y);
x = coordinate[0];
y = coordinate[1];
} else {
console.log(x, y, '++++++继续遍历', temp, ',填入数值', temp[0])
board[x][y] = temp[0];
console.log(board)
y === 8 ? (x += 1, y = 0) : y += 1;
}
}
}
if (x === 9 && y === 0) {
console.log('------数独已完成------');
console.log(board);
}
}
};
// 数独搜索回溯
function rollBack(board, x, y) {
while (x !== -1 || y !== 8) {
if (typeof board[x][y] !== 'number') { //跳过不是数字的部分
y === 0 ? (x -= 1, y = 8) : y -= 1;
} else {
let num = board[x][y]; //记录此处使用的数字
board[x][y] = '.'; //先还原当前位置的数独的值,再获取当前位置的可能值
let temp = getPossibleValue(board, x, y);
let index = temp.indexOf(num);
if (temp[index + 1]) {
// 决策需要填进去的数字
console.log(x, y, '++++++可以修改', temp, ',填入数值', temp[index + 1]);
board[x][y] = temp[index + 1];
break;
} else {
console.log(x, y, '此处没有可以修改的值了,数值还原成 "."', temp, index)
y === 0 ? (x -= 1, y = 8) : y -= 1;
}
}
}
if (x === -1 && y === 8) {
console.log('数独无解!')
}
return [x,y];
}
/**获取给定坐标的可能值的集合
*
* @param board 目标数独
* @param x row
* @param y col
* @returns {Array}
*/
function getPossibleValue(board, x, y) {
const len = board.length;
let existNums = [];
let restNum = [];
let m = ~~(x / 3);
let n = ~~(y / 3);
for (let i = 0; i < len; i++) {
let cell = board[m * 3 + ~~(i / 3)][n * 3 + i % 3];
if (board[x][i] !== '.') existNums.push(board[x][i]);
if (board[i][y] !== '.') existNums.push(board[i][y]);
if (cell !== '.') existNums.push(cell)
}
let map = {};
for (let i = 0, len = existNums.length; i < len; i++) {
if (!map[existNums[i]]) map[existNums[i]] = true;
}
for (let i = 1; i <= len; i++) {
if (!map[i]) restNum.push(i);
}
return restNum;
}
var 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"]
];
solveSudoku(board);
结语
刚开始函数是写成递归的形式的,但运行时递归调用栈溢出报错了。但奇怪的是在chrome控制台可以运行出解,但在node环境下就溢出了。最后无奈改成非递归版本的,最后成了如上所示的结果。
然后可以改进的部分有3点:
1. 一开始遍历一次数独,rest只有一个的位置先填入,这样可以给后面的决策更多的信息
2. 没有提供数独多解的答案
3. 前面提到的获取当前可能的值的集合中,排除法确定某个数的位置的决策逻辑没用上
对于第一个测试用例中其实数独所有位置的值都是确定的,即rest.length === 1
,5次遍历找到rest.length===1
的地方填入即可求解数独,所以在solveSudoku
函数的开头添加这样一段代码
let roundFinished = false;
let hasModified = false;
let nums = 0;
let i = 0;
let j = 0;
// 找出所有唯一解的格子
while (true) {
if (i === 0 && j === 0) {
nums = 0;
roundFinished = false;
hasModified = false;
}
if (i === 8 && j === 8) {
roundFinished = true;
}
if (board[i][j] === '.') {
let temp = getPossibleValue(board, i, j);
if (temp.length === 1) {
board[i][j] = temp[0] + '';
hasModified = true;
}
} else {
nums++;
}
if (roundFinished && !hasModified) {
break;
}
j === 8 ? (i += 1, j = 0) : j += 1;
i = i > 8 ? 0 : i;
}
if (nums === 81) return console.log('数独已完成!');
再次更新
可以输出数独的所有解了。
当搜索回溯到(0,0)
再回溯时即已经遍历完整个数独的所有可能性,此时搜索跳出
每次搜索到(8,8)
时就有一个解产生,记录下来即可
更新后代码如下:
/**
* @param {arr[][]} board
* @return {void} Do not return anything, modify board in-place instead.
*/
var solveSudoku = function (board) {
let result = []; //存放数独的所有可能解
fillCertainCell(board);
search(board, 0, 0);
function search(board, x, y) {
let temp;
// 主体遍历数独
while (x !== 9 || y !== 0) {
if (board[x][y] !== '.') { //跳过不需要填数字的部分
y === 8 ? (x += 1, y = 0) : y += 1;
} else {
// 决策需要填的数字
temp = getPossibleValue(board, x, y);
if (!temp.length) {
// 没有可以填的数字了 ->> 需要回溯
console.log(x, y, 'temp.length === 0,开始回滚')
let coordinate = rollBack(board, x, y);
x = coordinate[0];
y = coordinate[1];
// 全部搜索完的情况
if (x === -1 && y === 8) {
return printResult();
}
} else {
console.log(x, y, '++++++继续遍历', temp, ',填入数值', temp[0])
board[x][y] = temp[0];
y === 8 ? (x += 1, y = 0) : y += 1;
}
}
}
if (x === 9 && y === 0) {
result.push(deepCopy(board));
console.log('------数独已完成第' + result.length + '个解------');
let coordinate = rollBack(board, 8, 8)
search(board, coordinate[0], coordinate[1])
}
}
function printResult() {
if (!result.length) {
console.log('数独无解!')
} else {
console.log(`-------搜索完毕,数独有${result.length}个解---------`);
for (let i = 0; i < result.length; i++) {
console.log(`第${i + 1}个解:`);
console.log(result[i]);
}
}
}
};
/**给定数独,找出所有唯一确定值的格子
* 并进行原地修改
* @param board
*/
function fillCertainCell(board) {
let roundFinished = false;
let hasModified = false;
let nums = 0;
let i = 0;
let j = 0;
// 找出所有唯一解的格子
while (true) {
if (i === 0 && j === 0) {
nums = 0;
roundFinished = false;
hasModified = false;
}
if (i === 8 && j === 8) {
roundFinished = true;
}
if (board[i][j] === '.') {
let temp = getPossibleValue(board, i, j);
if (temp.length === 1) {
board[i][j] = temp[0] + '';
hasModified = true;
}
} else {
nums++;
}
if (roundFinished && !hasModified) {
break;
}
j === 8 ? (i += 1, j = 0) : j += 1;
i = i > 8 ? 0 : i;
}
if (nums === 81) {
console.log('数独已完成!有唯一解:');
console.log(board);
}
}
// 数独搜索回溯
function rollBack(board, x, y) {
while (x !== -1 || y !== 8) {
if (typeof board[x][y] !== 'number') { //跳过不是数字的部分
y === 0 ? (x -= 1, y = 8) : y -= 1;
} else {
let num = board[x][y]; //记录此处使用的数字
board[x][y] = '.'; //先还原当前位置的数独的值,再获取当前位置的可能值
let temp = getPossibleValue(board, x, y);
let index = temp.indexOf(num);
if (temp[index + 1]) {
// 决策需要填进去的数字
console.log(x, y, '++++++可以修改', temp, ',填入数值', temp[index + 1]);
board[x][y] = temp[index + 1];
break;
} else {
console.log(x, y, '此处没有可以修改的值了,数值还原成 "."', temp, index)
y === 0 ? (x -= 1, y = 8) : y -= 1;
}
}
}
return [x, y];
}
/**获取给定坐标的可能值的集合
*
* @param board 目标数独
* @param x row
* @param y col
* @returns {Array}
*/
function getPossibleValue(board, x, y) {
const len = board.length;
let existNums = [];
let restNum = [];
let m = ~~(x / 3);
let n = ~~(y / 3);
for (let i = 0; i < len; i++) {
let cell = board[m * 3 + ~~(i / 3)][n * 3 + i % 3];
if (board[x][i] !== '.') existNums.push(board[x][i]);
if (board[i][y] !== '.') existNums.push(board[i][y]);
if (cell !== '.') existNums.push(cell)
}
let map = {};
for (let i = 0, len = existNums.length; i < len; i++) {
if (!map[existNums[i]]) map[existNums[i]] = true;
}
for (let i = 1; i <= len; i++) {
if (!map[i]) restNum.push(i);
}
return restNum;
}
/**深拷贝多维数组
*
* @param arr
* @return {*}
*/
function deepCopy(arr) {
let copyArray = [];
if (Object.prototype.toString.call(arr) !== '[object Array]') return arr;
for (let i = 0; i < arr.length; i++) {
copyArray.push(deepCopy(arr[i]));
}
return copyArray;
}
var board = [
[".", ".", "9", "7", "4", "8", ".", ".", "."],
["7", ".", ".", ".", ".", ".", ".", ".", "."],
[".", "2", ".", ".", ".", "9", ".", ".", "."],
[".", ".", "7", ".", ".", ".", "2", "4", "."],
[".", "6", "4", ".", "1", ".", "5", "9", "."],
[".", "9", "8", ".", ".", ".", "3", ".", "."],
[".", ".", ".", "8", ".", "3", ".", "2", "."],
[".", ".", ".", ".", ".", ".", ".", ".", "6"],
[".", ".", ".", "2", "7", "5", "9", ".", "."]
];
solveSudoku(board);