使用遞歸回溯算法實現的前端解數獨遊戲

前言

最近一個多月來,我一直全身心投入於數據結構和算法的學習,一邊學習各類典型的算法類型,一邊瘋狂刷題,從一個對算法一無所知的小白,到如今leetcode破百的解題數,仍是頗有成就感的。邏輯思惟能力也是明顯的有所提升,不少算法當中的思想其實也是能夠應用到實際的開發當中的。html

由於算法學習整體來講,仍是比較枯燥的,就是不斷的刷題,前幾天,在刷遞歸回溯算法相關題型的時候,作到了一道解數獨的題目,感受頗有意思,花了一點時間作了一個純前端實現的解數獨遊戲,在這裏分享一下遞歸回溯法用來解數獨的具體實現。前端

介紹

解數獨遊戲地址,你們能夠點進去試玩一下,就是一個html文件,樣式上作的比較簡陋,沒作兼容性,通常低版本的瀏覽器可能打開會有問題。想看源碼的就直接右鍵查看源碼。算法

微信截圖_20210414232459.png

功能很簡單,生成一套數獨題目,能夠手動填入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-9 在每一行只能出現一次。
  • 數字 1-9 在每一列只能出現一次。
  • 數字 1-9 在每個以粗實線分隔的 3x3 宮內只能出現一次。

每個空白格都要選一個數字去填,有多少個空白格,作多少次選擇。咱們能夠想到遞歸,每次遞歸填當前的格子。不過咱們須要思考一下兩個點數據結構

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;
};
複製代碼

總結

沒啥說的,就是介紹了遞歸回溯算法在解數獨上的具體實現。

感謝

感謝您的閱讀,若是本文對你有幫助,就點個贊支持下吧。

相關文章
相關標籤/搜索