聊聊用 JavaScript 做数独

开发 前端
最近看到老婆天天在手机上玩数独,突然想起 N 年前刷 LeetCode 的时候,有个类似的算法题(37.解数独),是不是可以把这个算法进行可视化。

[[421904]]

最近看到老婆天天在手机上玩数独,突然想起 N 年前刷 LeetCode 的时候,有个类似的算法题(37.解数独),是不是可以把这个算法进行可视化。

说干就干,经过一个小时的实践,最终效果如下:

怎么解数独

解数独之前,我们先了解一下数独的规则:

数字 1-9 在每一行只能出现一次。

数字 1-9 在每一列只能出现一次。

数字 1-9 在每一个以粗实线分隔的九宫格( 3x3 )内只能出现一次。

接下来,我们要做的就是在每个格子里面填一个数字,然后判断这个数字是否违反规定。

填第一个格子

首先,在第一个格子填 1,发现在第一列里面已经存在一个 1,此时就需要擦掉前面填的数字 1,然后在格子里填上 2,发现数字在行、列、九宫格内均无重复。那么这个格子就填成功了。

填第二个格子

下面看第二个格子,和前面一样,先试试填 1,发现在行、列、九宫格内的数字均无重复,那这个格子也填成功了。

填第三个格子

下面看看第三个格子,由于前面两个格子,我们已经填过数字 1、2,所以,我们直接从数字 3 开始填。填 3 后,发现在第一行里面已经存在一个 3,然后在格子里填上 4,发现数字 4 在行和九宫格内均出现重复,依旧不成功,然后尝试填上数字 5,终于没有了重复数字,表示填充成功。

……

一直填……

填第九个格子

照这个思路,一直填到第九个格子,这个时候,会发现,最后一个数字 9 在九宫格内冲突了。而 9 已经是最后一个数字了,这里没办法填其他数字了,只能返回上一个格子,把第七个格子的数字从 8 换到 9,发现在九宫格内依然冲突。

此时需要替换上上个格子的数字(第六个格子)。直到没有冲突为止,所以在这个过程中,不仅要往后填数字,还要回过头看看前面的数字有没有问题,不停地尝试。

综上所述

解数独就是一个不断尝试的过程,每个格子把数字 1-9 都尝试一遍,如果出现冲突就擦掉这个数字,直到所有的格子都填完。

通过代码来实现

把上面的解法反映到代码上,就需要通过 递归 + 回溯 的思路来实现。

在写代码之前,先看看怎么把数独表示出来,这里参考 leetcode 上的题目:37. 解数独。

前面的这个题目,可以使用一个二维数组来表示。最外层数组内一共有 9 个数组,表示数独的 9 行,内部的每个数组内 9 字符分别对应数组的列,未填充的空格通过字符('.' )来表示。

  1. const sudoku = [ 
  2.   ['.''.''.''4''.''.''.''3''.'], 
  3.   ['7''.''4''8''.''.''1''.''2'], 
  4.   ['.''.''.''2''3''.''4''.''9'], 
  5.   ['.''4''.''5''.''9''.''8''.'], 
  6.   ['5''.''.''.''.''.''9''1''3'], 
  7.   ['1''.''.''.''8''.''2''.''4'], 
  8.   ['.''.''.''.''.''.''3''4''5'], 
  9.   ['.''5''1''9''4''.''7''2''.'], 
  10.   ['4''7''3''.''5''.''.''9''1'], 

知道如何表示数组后,我们再来写代码。

  1. const sudoku = [……] 
  2. // 方法接受行、列两个参数,用于定位数独的格子 
  3. function solve(row, col) { 
  4.   if (col >= 9) {  
  5.    // 超过第九列,表示这一行已经结束了,需要另起一行 
  6.     col = 0 
  7.     row += 1 
  8.     if (row >= 9) { 
  9.       // 另起一行后,超过第九行,则整个数独已经做完 
  10.       return true 
  11.     } 
  12.   } 
  13.   if (sudoku[row][col] !== '.') { 
  14.     // 如果该格子已经填过了,填后面的格子 
  15.     return solve(row, col + 1) 
  16.   } 
  17.   // 尝试在该格子中填入数字 1-9 
  18.   for (let num = 1; num <= 9; num++) { 
  19.     if (!isValid(row, col, num)) { 
  20.       // 如果是无效数字,跳过该数字 
  21.       continue 
  22.     } 
  23.     // 填入数字 
  24.     sudoku[row][col] = num.toString() 
  25.     // 继续填后面的格子 
  26.     if (solve(row, col + 1)) { 
  27.       // 如果一直到最后都没问题,则这个格子的数字没问题 
  28.       return true 
  29.     } 
  30.     // 如果出现了问题,solve 返回了 false 
  31.     // 说明这个地方要重填 
  32.     sudoku[row][col] = '.' // 擦除数字 
  33.   } 
  34.   // 数字 1-9 都填失败了,说明前面的数字有问题 
  35.   // 返回 FALSE,进行回溯,前面数字要进行重填 
  36.   return false 

上面的代码只是实现了递归、回溯的部分,还有一个 isValid 方法没有实现。该方法主要就是按照数独的规则进行一次校验。

  1. const sudoku = [……] 
  2. function isValid(row, col, num) { 
  3.   // 判断行里是否重复 
  4.   for (let i = 0; i < 9; i++) { 
  5.     if (sudoku[row][i] === num) { 
  6.       return false 
  7.     } 
  8.   } 
  9.   // 判断列里是否重复 
  10.   for (let i = 0; i < 9; i++) { 
  11.     if (sudoku[i][col] === num) { 
  12.       return false 
  13.     } 
  14.   } 
  15.   // 判断九宫格里是否重复 
  16.   const startRow = parseInt(row / 3) * 3 
  17.   const startCol = parseInt(col / 3) * 3 
  18.   for (let i = startRow; i < startRow + 3; i++) { 
  19.     for (let j = startCol; j < startCol + 3; j++) { 
  20.       if (sudoku[i][j] === num) { 
  21.         return false 
  22.       } 
  23.     } 
  24.   } 
  25.   return true 

通过上面的代码,我们就能解出一个数独了。

  1. const sudoku = [ 
  2.   ['.''.''.''4''.''.''.''3''.'], 
  3.   ['7''.''4''8''.''.''1''.''2'], 
  4.   ['.''.''.''2''3''.''4''.''9'], 
  5.   ['.''4''.''5''.''9''.''8''.'], 
  6.   ['5''.''.''.''.''.''9''1''3'], 
  7.   ['1''.''.''.''8''.''2''.''4'], 
  8.   ['.''.''.''.''.''.''3''4''5'], 
  9.   ['.''5''1''9''4''.''7''2''.'], 
  10.   ['4''7''3''.''5''.''.''9''1'
  11. function isValid(row, col, num) {……} 
  12. function solve(row, col) {……} 
  13. solve(0, 0) // 从第一个格子开始解 
  14. console.log(sudoku) // 输出结果 

输出结果

动态展示做题过程

有了上面的理论知识,我们就可以把这个做题的过程套到 react 中,动态的展示做题的过程,也就是文章最开始的 Gif 中的那个样子。

这里直接使用 create-react-app 脚手架快速启动一个项目

  1. npx create-react-app sudoku 
  2. cd sudoku 

打开 App.jsx ,开始写代码。

  1. import React from 'react'
  2. import './App.css'
  3.  
  4. class App extends React.Component { 
  5.   state = { 
  6.     // 在 state 中配置一个数独二维数组 
  7.     sudoku: [ 
  8.       ['.''.''.''4''.''.''.''3''.'], 
  9.       ['7''.''4''8''.''.''1''.''2'], 
  10.       ['.''.''.''2''3''.''4''.''9'], 
  11.       ['.''4''.''5''.''9''.''8''.'], 
  12.       ['5''.''.''.''.''.''9''1''3'], 
  13.       ['1''.''.''.''8''.''2''.''4'], 
  14.       ['.''.''.''.''.''.''3''4''5'], 
  15.       ['.''5''1''9''4''.''7''2''.'], 
  16.       ['4''7''3''.''5''.''.''9''1'
  17.     ] 
  18.   } 
  19.  
  20.  // TODO:解数独 
  21.   solveSudoku = async () => { 
  22.     const { sudoku } = this.state 
  23.   } 
  24.  
  25.   render() { 
  26.     const { sudoku } = this.state 
  27.     return ( 
  28.       <div className="container"
  29.         <div className="wrapper"
  30.           {/* 遍历二维数组,生成九宫格 */} 
  31.           {sudoku.map((list, row) => ( 
  32.             {/* div.row 对应数独的行 */} 
  33.             <div className="row" key={`row-${row}`}> 
  34.               {list.map((item, col) => ( 
  35.               {/* span 对应数独的每个格子 */} 
  36.                 <span key={`box-${col}`}>{ item !== '.' && item }</span> 
  37.               ))} 
  38.             </div> 
  39.           ))} 
  40.           <button onClick={this.solveSudoku}>开始做题</button> 
  41.         </div> 
  42.       </div> 
  43.     ); 
  44.   } 

九宫格样式

给每个格子加上一个虚线的边框,先让它有一点九宫格的样子。

  1. .row { 
  2.   display: flex; 
  3.   direction: row; 
  4.   /* 行内元素居中 */ 
  5.   justify-content: center; 
  6.   align-content: center; 
  7. .row span { 
  8.   /* 每个格子宽高一致 */ 
  9.   width: 30px; 
  10.   min-height: 30px; 
  11.   line-height: 30px; 
  12.   text-align: center; 
  13.   /* 设置虚线边框 */ 
  14.   border: 1px dashed #999; 

可以得到一个这样的图形:

接下来,需要给外边框和每个九宫格加上实线的边框,具体代码如下:

  1. /* 第 1 行顶部加上实现边框 */ 
  2. .row:nth-child(1) span { 
  3.   border-top: 3px solid #333; 
  4. /* 第 3、6、9 行底部加上实现边框 */ 
  5. .row:nth-child(3n) span { 
  6.   border-bottom: 3px solid #333; 
  7. /* 第 1 列左边加上实现边框 */ 
  8. .row span:first-child { 
  9.   border-left: 3px solid #333; 
  10.  
  11. /* 第 3、6、9 列右边加上实现边框 */ 
  12. .row span:nth-child(3n) { 
  13.   border-right: 3px solid #333; 

这里会发现第三、六列的右边边框和第四、七列的左边边框会有点重叠,第三、六行的底部边框和第四、七行的顶部边框也会有这个问题,所以,我们还需要将第四、七列的左边边框和第三、六行的底部边框进行隐藏。

  1. .row:nth-child(3n + 1) span { 
  2.   border-top: none; 
  3. .row span:nth-child(3n + 1) { 
  4.   border-left: none; 

做题逻辑

样式写好后,就可以继续完善做题的逻辑了。

  1. class App extends React.Component { 
  2.   state = { 
  3.     // 在 state 中配置一个数独二维数组 
  4.     sudoku: [……] 
  5.   } 
  6.  
  7.   solveSudoku = async () => { 
  8.     const { sudoku } = this.state 
  9.     // 判断填入的数字是否有效,参考上面的代码,这里不再重复 
  10.     const isValid = (row, col, num) => { 
  11.       …… 
  12.     } 
  13.     // 递归+回溯的方式进行解题 
  14.    const solve = async (row, col) => { 
  15.       if (col >= 9) {  
  16.         col = 0 
  17.         row += 1 
  18.         if (row >= 9) return true 
  19.       } 
  20.       if (sudoku[row][col] !== '.') { 
  21.         return solve(row, col + 1) 
  22.       } 
  23.       for (let num = 1; num <= 9; num++) { 
  24.         if (!isValid(row, col, num)) { 
  25.           continue 
  26.         } 
  27.   
  28.         sudoku[row][col] = num.toString() 
  29.         this.setState({ sudoku }) // 填了格子之后,需要同步到 state 
  30.  
  31.         if (solve(row, col + 1)) { 
  32.           return true 
  33.         } 
  34.  
  35.         sudoku[row][col] = '.' 
  36.         this.setState({ sudoku }) // 填了格子之后,需要同步到 state 
  37.       } 
  38.       return false 
  39.     } 
  40.     // 进行解题 
  41.     solve(0, 0) 
  42.   } 
  43.  
  44.   render() { 
  45.     const { sudoku } = this.state 
  46.     return (……) 
  47.   } 

对比之前的逻辑,这里只是在对数独的二维数组填空后,调用了 this.setState 将 sudoku 同步到了 state 中。

  1. function solve(row, col) { 
  2.    …… 
  3.    sudoku[row][col] = num.toString() 
  4. +  this.setState({ sudoku }) 
  5.   …… 
  6.    sudoku[row][col] = '.' 
  7. +  this.setState({ sudoku }) // 填了格子之后,需要同步到 state 

在调用 solveSudoku 后,发现并没有出现动态的效果,而是直接一步到位的将结果同步到了视图中。

这是因为 setState 是一个伪异步调用,在一个事件任务中,所以的 setState 都会被合并成一次,需要看到动态的做题过程,我们需要将每一次 setState 操作放到该事件流之外,也就是放到 setTimeout 中。更多关于 setState 异步的问题,可以参考我之前的文章:React 中 setState 是一个宏任务还是微任务?

  1. solveSudoku = async () => { 
  2.   const { sudoku } = this.state 
  3.   // 判断填入的数字是否有效,参考上面的代码,这里不再重复 
  4.   const isValid = (row, col, num) => { 
  5.     …… 
  6.   } 
  7.   // 脱离事件流,调用 setState 
  8.   const setSudoku = async (row, col, value) => { 
  9.     sudoku[row][col] = value 
  10.     return new Promise(resolve => { 
  11.       setTimeout(() => { 
  12.         this.setState({ 
  13.           sudoku 
  14.         }, () => resolve()) 
  15.       }) 
  16.     }) 
  17.   } 
  18.   // 递归+回溯的方式进行解题 
  19.   const solve = async (row, col) => { 
  20.     …… 
  21.     for (let num = 1; num <= 9; num++) { 
  22.       if (!isValid(row, col, num)) { 
  23.         continue 
  24.       } 
  25.  
  26.    await setSudoku(row, col, num.toString()) 
  27.  
  28.       if (await solve(row, col + 1)) { 
  29.         return true 
  30.       } 
  31.  
  32.    await setSudoku(row, col, '.'
  33.     } 
  34.     return false 
  35.   } 
  36.   // 进行解题 
  37.   solve(0, 0) 

最后效果如下:

本文转载自微信公众号「自然醒的笔记本」,可以通过以下二维码关注。转载本文请联系自然醒的笔记本公众号。

 

责任编辑:武晓燕 来源: 自然醒的笔记本
相关推荐

2023-11-06 11:33:15

C++数独

2021-01-07 07:53:10

JavaScript内存管理

2021-10-17 22:40:51

JavaScript开发 框架

2022-07-29 14:47:34

数独Sudoku鸿蒙

2013-06-20 10:52:37

算法实践数独算法数独源码

2022-02-09 11:02:16

JavaScript前端框架

2021-01-31 23:54:23

数仓模型

2013-06-17 12:44:38

WP7开发Windows Pho数独游戏

2022-10-19 15:27:36

数独Sudoku鸿蒙

2022-10-18 15:45:17

数独Sudoku鸿蒙

2022-10-19 15:19:53

数独Sudoku鸿蒙

2019-07-23 15:04:54

JavaScript调用栈事件循环

2021-06-02 09:01:19

JavaScript 前端异步编程

2023-11-20 08:01:38

并发处理数Tomcat

2021-09-08 08:55:45

Javascript 高阶函数前端

2022-02-23 09:03:29

JavaScript开发命名约定

2022-03-01 17:16:16

数仓建模ID Mapping

2022-02-23 08:18:06

nginx前端location

2020-09-24 16:40:20

人工智能量子计算技术

2020-06-15 08:13:42

Linux服务端并发数
点赞
收藏

51CTO技术栈公众号