最近一個多月來,我一直全身心投入於數據結構和算法的學習,一邊學習各類典型的算法類型,一邊瘋狂刷題,從一個對算法一無所知的小白,到如今leetcode
破百的解題數,仍是頗有成就感的。邏輯思惟能力也是明顯的有所提升,不少算法當中的思想其實也是能夠應用到實際的開發當中的。html
由於算法學習整體來講,仍是比較枯燥的,就是不斷的刷題,前幾天,在刷遞歸回溯算法相關題型的時候,作到了一道解數獨
的題目,感受頗有意思,花了一點時間作了一個純前端實現的解數獨
遊戲,在這裏分享一下遞歸回溯法用來解數獨的具體實現。前端
解數獨遊戲地址,你們能夠點進去試玩一下,就是一個html文件,樣式上作的比較簡陋,沒作兼容性,通常低版本的瀏覽器可能打開會有問題。想看源碼的就直接右鍵查看源碼。算法
功能很簡單,生成一套數獨題目,能夠手動填入1-9的數據,須要使每一行,每一列,每個3X3的框中都存在1-9這9個數字。而後能夠提交驗證看是否答對。數組
PS:生成一套只有惟一解的數獨這塊的算法有點難搞,我嘗試了不少思路都失敗了,因此這裏的數獨題目目前都是我直接從題庫中隨機獲取的。瀏覽器
按上圖所示的數據,解數獨能夠抽象成微信
const 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"]
]
複製代碼
給你一個二維數組,假設給定的數獨只有惟一解,你須要編寫一個程序,填充空格這些空格,而且須要知足如下三個條件。markdown
每個空白格都要選一個數字去填,有多少個空白格,作多少次選擇。咱們能夠想到遞歸,每次遞歸填當前的格子。不過咱們須要思考一下兩個點數據結構
1.每次遞歸的約束條件要怎麼去處理?app
2.這是個二維數組,怎麼使用一個遞歸方法去處理每一層的數組?數據結構和算法
數獨的約束條件要求咱們每一行,每一列,每個3X3的宮內,1-9都只能使用一次。
若是按照暴力解法來考慮,每次填入一個值的時候,循環遍歷當前節點的行,列,宮,若是不存在重複的值,才能填入值,而後繼續遞歸。
不過咱們能夠作一些優化,由於每次都循環行列和宮,花費的性能比較大,咱們能夠定義三個變量用來記錄每一行,每一列,每個宮中使用過的數字,從循環變成查找表的形式,提升性能。
不少遞歸回溯的題其實只是對於一個一維數組或者字符串的遞歸,可是這裏是二維數組。因此咱們須要變通,遞歸時候定義x,y兩個值表明當前節點的x軸和y軸。先從第一行第一位開始遞歸,x軸不變,每次遞歸只讓y軸加1,當y軸等於9時,說明當前序號爲x的數組已經遞歸完成。咱們把x加1,y歸0。繼續遞歸下一行的數組。
var solveSudoku = function(board) {
// 定義三個數組來記錄,每一行,每一行,每個3X3裏已經被使用過的參數
let lineX = [];
let lineY = [];
let area3x3 = [];
for (let n = 0; n < 9; n ++) {
lineX[n] = {};
lineY[n] = {};
if (((n + 1) % 3) === 0) {
const curIndex = (n + 1) / 3;
area3x3[curIndex - 1] = [{}, {}, {}];
}
}
// 用來增長(刪除)記錄,若是已經被使用過,將當前位置置爲true
const toggleCache = (i, j, val, type) => {
const flag = (type === 'add' ? true : false);
lineX[i][val] = flag;
lineY[j][val] = flag;
area3x3[Math.floor(i / 3)][Math.floor(j / 3)][val] = flag;
};
// 判斷當前數據是否在當前行,列,3x3中存在
const hasCache = (i, j, val) => {
if(lineX[i][val]) return true;
if(lineY[j][val]) return true;
if(area3x3[Math.floor(i / 3)][Math.floor(j / 3)][val]) return true;
return false;
};
// 初始化,將初始數據傳入記錄中
for (let i = 0; i < 9; i ++) {
for (let j = 0; j < 9; j ++) {
const val = board[i][j];
if (val !== '.') toggleCache(i, j, val, 'add');
}
}
// 遞歸方法
const dfs = (xIndex, yIndex) => {
// 當x軸遞歸到9時,結束遞歸。
// 由於本題都只有惟一解,因此須要return一個true,用來計算出值以後,提早退出
if (xIndex === 9) return true;
// 當y軸遞歸到9時,將x加1,繼續下一行的遞歸
if (yIndex === 9) return dfs(xIndex + 1, 0);
if (board[xIndex][yIndex] === '.') {
// 若是當前沒有值,遍歷1-9
for (let v = 1; v <= 9; v ++) {
// 判斷遍歷的v是否知足同一行,同一列,3x3都沒有被使用過
if (!hasCache(xIndex, yIndex, String(v))) {
// 遞歸
board[xIndex][yIndex] = String(v);
toggleCache(xIndex, yIndex, v, 'add');
// 若是獲得的返回值是true,說明已經獲得值了,不須要再計算後面的值了,直接退出
if (dfs(xIndex, yIndex + 1)) return true;
// 回溯
board[xIndex][yIndex] = '.';
toggleCache(xIndex, yIndex, v, 'remove');
}
}
} else {
// 若是當有初始值,跳到下一個節點
return dfs(xIndex, yIndex + 1);
}
};
dfs(0, 0);
return board;
};
複製代碼
沒啥說的,就是介紹了遞歸回溯算法在解數獨上的具體實現。
感謝您的閱讀,若是本文對你有幫助,就點個贊支持下吧。