leetCode 37 解數獨 Soduko Solver

leetCode 37 解數獨 Soduko Solver

以前比較喜歡玩數獨,一直想嘗試一下編程解數獨,練習算法時看到LeetCode上正好有這樣一題,於是決定做一下。做出來的過程中確實花費了一番功夫,遂記錄下來整個過程。

整理思路

開始時一頭霧水,於是回想自己做數獨的邏輯與策略:
1. 有沒有哪一個位置開始就可以確定值的?把能確定值的空格全部填上
2. 當剩餘的空格爲多解時,找解最少的開始試

解題方法

  1. (0,0)開始遍歷數獨,當座標(x, y)==='.'時,獲取(x,y)位置的可能取值集合rest
  2. rest.length !== 0,將(x,y)位置的值替換成rest[0],遍歷繼續
  3. rest.length === 0則開始回溯
  4. 遍歷完(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);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章