当前位置: 首页 > Web前端 > JavaScript

用JavaScript做数独

时间:2023-03-27 01:29:52 JavaScript

最近看到老婆天天在手机上玩数独。突然想起N年前刷LeetCode的时候,有一道类似的算法题(37.SolvingSudoku)。有没有可能解决这个算法?可视化。照做吧,经过一个小时的练习,最终效果如下:如何解数独在解数独之前,我们先来了解一下数独的规则:数字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.SolveSudoku。上一题可以用二维数组来表示。最外层数组有9个数组,代表9行数独。每个内部数组中的9个字符对应数组的列,未填充的空格用字符('.')表示。constsudoku=[['.','.','.','4','.','.','.','3','.'],['7','.','4','8','.','.','1','.','2'],['.','.','.','2','3','.','4','.','9'],['.','4','.','5','.','9','.','8','.'],['5','.','.','.','.','.','9','1','3'],['1','.','.','.','8','.','2','.','4'],['.','.','.','.','.','.','3','4','5'],['.','5','1','9','4','.','7','2','.'],['4','7','3','.','5','.','.','9','1']]知道如何represent数组之后,我们来写代码。constsudoku=[…]//该方法接受行和列两个参数,用于定位数独函数的格子solve(row,col){if(col>=9){//如果超过第九个column,表示这一行已经结束,需要开始另一行。col=0row+=1if(row>=9){//开始新的一行后,如果超过第九行,则整个数独已经完成returntrue}}if(sudoku[row][col]!=='.'){//如果网格已经被填充,填充下一个网格returnsolve(row,col+1)}//尝试用数字1-9填充网格for(letnum=1;num<=9;num++){if(!isValid(row,col,num)){//如果是无效数字,跳过数字继续}//填入数字sudoku[row][col]=num.toString()//继续填充格子if(solve(row,col+1)){//如果到最后都没有问题,那么这个格子的个数就OKreturntrue}//如果有问题,solve返回false//表示这个地方需要重新填充sudoku[row][col]='.'//擦除数字}//数字1-9填写失败,说明之前的数字有问题//返回FALSE,回溯,之前的数字需要重新填写返回false}上面的代码只实现了递归和回溯部分,还有一个isValid方法没有实现。该方法主要是根据数独的规则进行校验。constsudoku=[…]functionisValid(row,col,num){//判断行是否重复for(leti=0;i<9;i++){if(sudoku[row][i]===num){returnfalse}}//判断该列是否重复for(leti=0;i<9;i++){if(sudoku[i][col]===num){returnfalse}}//判断九宫格是否重复conststartRow=parseInt(row/3)*3conststartCol=parseInt(col/3)*3for(leti=startRow;i{const{sudoku}=this.state}render(){const{sudoku}=this.statereturn({/*遍历二维数组生成一个九-squaregrid*/}{sudoku.map((list,row)=>({/*div.row对应数独行*/}{list.map((item,col)=>({/*span对应数独的每个格子*/}{item!=='.'&&item}))}

))}开始解题
);}}九宫格风格给每个格子加上虚线边框,先让它看起来像九宫格.row{显示:flex;方向:行;/*将行中的元素居中*/justify-content:center;align-content:center;}.rowspan{/*每个网格的宽度和高度都相同*/width:30px;最小高度:30px;行高:30px;文本对齐:居中;/*Setdashedborder*/border:1pxdashed#999;}可以得到这样的图形:接下来需要给外边框和每个九格格添加实线边框,具体代码如下:/*在第一行顶部添加实现边框*/.row:nth-child(1)span{border-top:3pxsolid#333;}/*第三和第六、底部添加边框row9*/.row:nth-child(3n)span{border-bottom:3pxsolid#333;}/*在第一列的左边添加边框*/.rowspan:first-child{border-left:3pxsolid#333;}/*在第3、6、9列右侧添加边框*/.rowspan:nth-child(3n){border-right:3pxsolid#333;}这里会发现,第三和第六列的右边框和第四和第七列的左边框会重叠一点,第三和第六行的底部边框和第四和第七行的顶部边框也会有这个问题,所以我们还需要隐藏第四列和第七列的左边框以及第三行和第六行的底部边框。.row:nth-child(3n+1)span{border-top:none;}.rowspan:nth-child(3n+1){border-left:none;}写完问题的逻辑风格后,你可以继续完善题目的逻辑。classAppextendsReact.Component{state={//配置一个数独二维数组sudokuinstate:[…]}solveSudoku=async()=>{const{sudoku}=this.state//判断填充数是否为数是否有效,参考上面代码,这里不再赘述constisValid=(row,col,num)=>{...}//通过递归+回溯解决问题constsolve=async(row,col)=>{if(col>=9){col=0row+=1if(row>=9)返回true}if(sudoku[row][col]!=='.'){returnsolve(row,col+1)}for(letnum=1;num<=9;num++){if(!isValid(row,col,num)){continue}sudoku[row][col]=num.toString()this.setState({sudoku})//填满网格后,需要同步到stateif(solve(row,col+1)){returntrue}sudoku[row][col]='.'this.setState({sudoku})//填完格子后,需要同步到state}returnfalse}//解决问题solve(0,0)}render(){const{sudoku}=this.statereturn(…)}}与之前的逻辑相比,调用this.setState将数独同步到数独二维数组填空后的状态。functionsolve(row,col){...sudoku[row][col]=num.toString()+this.setState({sudoku})......sudoku[row][col]='.'+this.setState({sudoku})//填完格子后需要先同步到state}再调用solveSudoku最后发现没有动态效果,一步一步直接将结果同步到视图。这是因为setState是一个伪异步调用。在一个事件任务中,所有的setState都会合并为一个。我们要看到做题的动态过程。我们需要把每一个setState操作都放在事件流之外,也就是放在setTimeout里面。更多关于setState异步的问题可以参考我之前的文章:React中的setState是宏任务还是微任务?solveSudoku=async()=>{const{sudoku}=this.state//判断填写的数字是否有效,参考上面代码,这里不再赘述constisValid=(row,col,num)=>{...}//离开事件流,调用setStateconstsetSudoku=async(row,col,value)=>{sudoku[row][col]=valuereturnnewPromise(resolve=>{setTimeout(()=>{this.setState({sudoku},()=>resolve())})})}//递归+回溯解决问题constsolve=async(row,col)=>{…for(letnum=1;num<=9;num++){if(!isValid(row,col,num)){continue}awaitsetSudoku(row,col,num.toString())if(awaitsolve(row,col+1)){returntrue}awaitsetSudoku(row,col,'.')}returnfalse}//解决问题solve(0,0)}最终效果如下: