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