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);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值