前端算法必刷题系列[63]

455 阅读4分钟

这是我参与更文挑战的第 13 天,活动详情查看 更文挑战

这个系列没啥花头,就是纯 leetcode 题目拆解分析,不求用骚气的一行或者小众取巧解法,而是用清晰的代码和足够简单的思路帮你理清题意。让你在面试中再也不怕算法笔试。

120. 有效的数独 (valid-sudoku)

标签

  • Array
  • 中等

题目

leetcode 传送门

这里不贴题了,leetcode打开就行,题目大意:

请你判断一个 9x9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可

  • 数字 1-9 在每一行只能出现一次。
  • 数字 1-9 在每一列只能出现一次。
  • 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。

注意:

  • 一个有效的数独(部分已被填充)不一定是可解的
  • 只需要根据以上规则,验证已经填入的数字是否有效即可。

image.png

示例 1:

输入: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"]]
输出:true

示例 2:

输入:board = 
[["8","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"]]
输出:false
解释:除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。 
但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。

基本思路

判断是否合法其实比较简单,就是用横,纵,子盒子(3x3)设置3个数组,然后依次判断每个格子,这个格子里的数如果跟已知当前行列或子盒子的数重复,就说明不合法,否则就是合法,并在这三个数组中推入,表示该位置填上当前数。

行列,用 i, j 直接判断行列即可,子盒子的 index 判断稍微复杂些,看这个图

image.png

box_index = (i / 3) * 3 + j / 3,其中 /整数除法

知道 (i, j) 我们就能知道是哪个子盒子,也就是根据坐标,获取所在的子盒子的索引

写法实现

const isValidSudoku = (board) => {
  // 横,纵,小盒子的初始化,表示已经填上的数字,开始都是空的
  let rowsUsed = new Array(9).fill(0).map(it => [])
  let colsUsed = new Array(9).fill(0).map(it => [])
  let boxesUsed = new Array(9).fill(0).map(it => [])

  for (let i = 0; i < 9; i++) {
    for (let j = 0; j < 9; j++) {

      const cur = board[i][j];

      // 空的无所谓,跳过,因为我们判断的是现有填上的是否合法
      if (cur !== '.') {

        // 当前横排已经有此元素,直接返回 false
        if (rowsUsed[i].includes(cur)) {
          return false
        } else {
          // 否则就填上,然后推入该已用数组
          rowsUsed[i].push(cur)
        }
        
        // 竖排相同道理
        if (colsUsed[j].includes(cur)) {
          return false;
        } else {
          colsUsed[j].push(cur);
        }

        // 9个小方格稍微复杂点,算出大方块的 index,然后还是相同的判断9个数是否被用过
        const boxIndex = Math.floor(i / 3) * 3 + Math.floor(j / 3);

        if (boxesUsed[boxIndex].includes(cur)) {
          return false;
        } else {
          boxesUsed[boxIndex].push(cur);
        }
      }

    }
  }

  return true;
};

console.log(isValidSudoku(
[[".","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"]]
));

121. 解数独 (sudoku-solver)

标签

  • DFS + 剪枝
  • 困难

题目

leetcode 传送门

这里不贴题了,leetcode打开就行,题目大意:

编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则:

数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图) 数独部分空格内已填入了数字,空白格用 '.' 表示。

image.png image.png

示例 1:

输入: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"]]
输出:
[["5","3","4","6","7","8","9","1","2"],
["6","7","2","1","9","5","3","4","8"],
["1","9","8","3","4","2","5","6","7"],
["8","5","9","7","6","1","4","2","3"],
["4","2","6","8","5","3","7","9","1"],
["7","1","3","9","2","4","8","5","6"],
["9","6","1","5","3","7","2","8","4"],
["2","8","7","4","1","9","6","3","5"],
["3","4","5","2","8","6","1","7","9"]]
解释:输入的数独如上图所示,唯一有效的解决方案如下所示:

基本思路

如果对 DFS + 回溯 问题不熟悉请先看看之前这个系列的问题

另外数独和之前的文章 N皇后 有点类似,可以先看看

这种题目基本都可以用暴力法求解,就是穷举,但是我们应该多思考更好的解决方案,如果你是个玩数独的高手,或者简单来说正常思维,我们都会在数独中,先找那种好填的填写,什么是好填? 如果一行中,全都已经写好就差一个数没填,那是不是只能填那个数,你只有一种填法,只需要枚举一个数就行了,在深度遍历的路上相当于就一条道,简单吧。当然现实不可能这么简单,但思想就是,先从选择少的那些路开始走,这能大大减少 DFS 的复杂度,剪去了许多无用枝条

我们也可以做一次预处理,先把每个空格子能填的数都给他算好,然后同样 DFS 先遍历少的,然后回溯,最终找到一个正确解。详细做法根据我的代码注释可以清楚地理解,代码虽多,但我们的思路一定要清晰,不要畏惧,看懂了很简单。

写法实现

const solveSudoku = (board) => {
  // 枚举值
  const enumVal = ['1', '2', '3', '4', '5', '6', '7', '8', '9']; 

  const rows = new Array(9).fill(0).map(it => new Set(enumVal));    // 每一行的 可选数集
  const cols = new Array(9).fill(0).map(it => new Set(enumVal));    // 每一列的 可选数集
  const blocks = new Array(9).fill(0).map(it => new Set(enumVal));  // 每一子盒的 可选数集
  
  // 根据坐标,获取所在的子盒子的索引
  const getBoxIndex = (i, j) => {
    return Math.floor(i / 3) * 3 + Math.floor(j / 3);
  };

  // 根据现有的已填的数字,精简可选数据集,把不能填写的数字从可选数据集中删除
  for (let i = 0; i < 9; i++) {    
    for (let j = 0; j < 9; j++) {
      if (board[i][j] !== ".") {
        // 当前行出现过这个数字,这个数字就不能在这一行出现,就应该从这一行的`可选`数据集中删除
        // 可选的意思就是 选了之后仍然合法
        rows[i].delete(board[i][j]);
        cols[j].delete(board[i][j]);
        blocks[getBoxIndex(i, j)].delete(board[i][j]);
      }
    }
  }

  const dfs = (i, j) => {
    // 列超过最边界坐标
    if (j > 8) {
      // 行+1
      i++;
      // 列回到起点
      j = 0;
      if (i > 8) {
        // 都填完了
        return true;
      }
    }

    // 如果不是空白格,递归填下一格
    if (board[i][j] !== ".") {
      return dfs(i, j + 1)
    } 

    const boxIndex = getBoxIndex(i, j); // 获取所在小框的索引

    // 枚举出所有选择:1-9
    for (let num = 1; num <= 9; num++) { 
      const cur = String(num);
      // 当前选择必须在三个set中都存在,如果有一个不存在,就说明发生了冲突,跳过该选择,这就是剪枝
      if (!rows[i].has(cur) || !cols[j].has(cur) || !blocks[boxIndex].has(cur)) continue;

      // 作出选择,填上该数
      board[i][j] = cur;
      // 删掉这个可填选项
      rows[i].delete(cur);  
      cols[j].delete(cur);
      blocks[boxIndex].delete(cur);

      if (dfs(i, j + 1)) return true; // 如果基于当前选择,填下一个,最后可解出数独,直接返回真
      
      // 回溯,恢复为空白格
      board[i][j] = ".";
      // 将之前删掉的当前数字,加回来
      rows[i].add(cur);     
      cols[j].add(cur);
      blocks[boxIndex].add(cur);
    }

    // 尝试了1-9,每个都往下递归,都不能做完,返回false
    return false;  
  };

  // 填格子的起点
  dfs(0, 0);
  return board;
};

let 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"]]

console.log(solveSudoku(board))

另外向大家着重推荐下这个系列的文章,非常深入浅出,对前端进阶的同学非常有作用,墙裂推荐!!!核心概念和算法拆解系列

今天就到这儿,想跟我一起刷题的小伙伴可以加我微信哦 点击此处交个朋友 Or 搜索我的微信号infinity_9368,可以聊天说地 加我暗号 "天王盖地虎" 下一句的英文,验证消息请发给我 presious tower shock the rever monster,我看到就通过,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧

参考