利用JavaScript做数独的完整实现过程

目录
  • 前言
  • 怎么解数独
    • 填第一个格子
    • 填第二个格子
    • 填第三个格子
    • 填第九个格子
    • 综上所述
  • 通过代码来实现
  • 动态展示做题过程
    • 九宫格样式
    • 做题逻辑
  • 总结

前言

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

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

怎么解数独

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

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 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 字符分别对应数组的列,未填充的空格通过字符('.' )来表示。

const sudoku = [
  ['.', '.', '.', '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'],
]

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

const sudoku = [……]
// 方法接受行、列两个参数,用于定位数独的格子
function solve(row, col) {
  if (col >= 9) {
   // 超过第九列,表示这一行已经结束了,需要另起一行
    col = 0
    row += 1
    if (row >= 9) {
      // 另起一行后,超过第九行,则整个数独已经做完
      return true
    }
  }
  if (sudoku[row][col] !== '.') {
    // 如果该格子已经填过了,填后面的格子
    return solve(row, col + 1)
  }
  // 尝试在该格子中填入数字 1-9
  for (let num = 1; num <= 9; num++) {
    if (!isValid(row, col, num)) {
      // 如果是无效数字,跳过该数字
      continue
    }
    // 填入数字
    sudoku[row][col] = num.toString()
    // 继续填后面的格子
    if (solve(row, col + 1)) {
      // 如果一直到最后都没问题,则这个格子的数字没问题
      return true
    }
    // 如果出现了问题,solve 返回了 false
    // 说明这个地方要重填
    sudoku[row][col] = '.' // 擦除数字
  }
  // 数字 1-9 都填失败了,说明前面的数字有问题
  // 返回 FALSE,进行回溯,前面数字要进行重填
  return false
}

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

const sudoku = [……]
function isValid(row, col, num) {
  // 判断行里是否重复
  for (let i = 0; i < 9; i++) {
    if (sudoku[row][i] === num) {
      return false
    }
  }
  // 判断列里是否重复
  for (let i = 0; i < 9; i++) {
    if (sudoku[i][col] === num) {
      return false
    }
  }
  // 判断九宫格里是否重复
  const startRow = parseInt(row / 3) * 3
  const startCol = parseInt(col / 3) * 3
  for (let i = startRow; i < startRow + 3; i++) {
    for (let j = startCol; j < startCol + 3; j++) {
      if (sudoku[i][j] === num) {
        return false
      }
    }
  }
  return true
}

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

const sudoku = [
  ['.', '.', '.', '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']
]
function isValid(row, col, num) {……}
function solve(row, col) {……}
solve(0, 0) // 从第一个格子开始解
console.log(sudoku) // 输出结果

动态展示做题过程

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

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

npx create-react-app sudoku
cd sudoku

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

import React from 'react';
import './App.css';

class App extends React.Component {
  state = {
    // 在 state 中配置一个数独二维数组
    sudoku: [
      ['.', '.', '.', '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']
    ]
  }

 // TODO:解数独
  solveSudoku = async () => {
    const { sudoku } = this.state
  }

  render() {
    const { sudoku } = this.state
    return (
      <div className="container">
        <div className="wrapper">
          {/* 遍历二维数组,生成九宫格 */}
          {sudoku.map((list, row) => (
            {/* div.row 对应数独的行 */}
            <div className="row" key={`row-${row}`}>
              {list.map((item, col) => (
              {/* span 对应数独的每个格子 */}
                <span key={`box-${col}`}>{ item !== '.' && item }</span>
              ))}
            </div>
          ))}
          <button onClick={this.solveSudoku}>开始做题</button>
        </div>
      </div>
    );
  }
}

九宫格样式

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

.row {
  display: flex;
  direction: row;
  /* 行内元素居中 */
  justify-content: center;
  align-content: center;
}
.row span {
  /* 每个格子宽高一致 */
  width: 30px;
  min-height: 30px;
  line-height: 30px;
  text-align: center;
  /* 设置虚线边框 */
  border: 1px dashed #999;
}

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

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

/* 第 1 行顶部加上实现边框 */
.row:nth-child(1) span {
  border-top: 3px solid #333;
}
/* 第 3、6、9 行底部加上实现边框 */
.row:nth-child(3n) span {
  border-bottom: 3px solid #333;
}
/* 第 1 列左边加上实现边框 */
.row span:first-child {
  border-left: 3px solid #333;
}

/* 第 3、6、9 列右边加上实现边框 */
.row span:nth-child(3n) {
  border-right: 3px solid #333;
}

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

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

做题逻辑

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

class App extends React.Component {
  state = {
    // 在 state 中配置一个数独二维数组
    sudoku: [……]
  }

  solveSudoku = async () => {
    const { sudoku } = this.state
    // 判断填入的数字是否有效,参考上面的代码,这里不再重复
    const isValid = (row, col, num) => {
      ……
    }
    // 递归+回溯的方式进行解题
   const solve = async (row, col) => {
      if (col >= 9) {
        col = 0
        row += 1
        if (row >= 9) return true
      }
      if (sudoku[row][col] !== '.') {
        return solve(row, col + 1)
      }
      for (let num = 1; num <= 9; num++) {
        if (!isValid(row, col, num)) {
          continue
        }

        sudoku[row][col] = num.toString()
        this.setState({ sudoku }) // 填了格子之后,需要同步到 state

        if (solve(row, col + 1)) {
          return true
        }

        sudoku[row][col] = '.'
        this.setState({ sudoku }) // 填了格子之后,需要同步到 state
      }
      return false
    }
    // 进行解题
    solve(0, 0)
  }

  render() {
    const { sudoku } = this.state
    return (……)
  }
}

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

function solve(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
  // 判断填入的数字是否有效,参考上面的代码,这里不再重复
  const isValid = (row, col, num) => {
    ……
  }
  // 脱离事件流,调用 setState
  const setSudoku = async (row, col, value) => {
    sudoku[row][col] = value
    return new Promise(resolve => {
      setTimeout(() => {
        this.setState({
          sudoku
        }, () => resolve())
      })
    })
  }
  // 递归+回溯的方式进行解题
  const solve = async (row, col) => {
    ……
    for (let num = 1; num <= 9; num++) {
      if (!isValid(row, col, num)) {
        continue
      }

   await setSudoku(row, col, num.toString())

      if (await solve(row, col + 1)) {
        return true
      }

   await setSudoku(row, col, '.')
    }
    return false
  }
  // 进行解题
  solve(0, 0)
}

最后效果如下:

总结

到此这篇关于利用JavaScript做数独的文章就介绍到这了,更多相关JavaScript做数独内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • javascript实现数独解法

    生生把写过的java版改成javascript版,第一次写,很不专业,见谅.唉,我是有多闲. 复制代码 代码如下: var Sudoku = {     init: function (str) {         this.blank = [];         this.fixed = [];         this.cell = [];         this.trials=[];         for (i = 0; i < 81; i++) {             var c

  • JavaScript遍历求解数独问题的主要思路小结

    数独规则 数独游戏,经典的为9×9=81个单元格组成的九宫格,同时也形成了3×3=9个小九宫格,要求在81个小单元格中填入数字1~9,并且数字在每行每列及每个小九宫格中都不能重复. 数独技巧 直观法 候选数法 相关二十格:一个数字只与其所在行列及小九宫格的二十格相关 我的思路 精心设计了有效性判定函数,最多一次遍历81个小单元格就能做出方案的有效性判定. 同理设计了相关20格判定,一次0~9的循环就完成有效性判定. 用数组模拟堆栈,为搜索提供回溯信息. 利用对象具有map性质,来辅助判断方案的有

  • javascript sudoku 数独智力游戏生成代码

    复制代码 代码如下: <p><input value="Get New SuDoKu" type="button" onclick="onLoadTable()" id="refreshButton" /></p> <table border="1" style="border-color: Red;" id="mainTable&qu

  • JS实现判断有效的数独算法示例

    本文实例讲述了JS实现判断有效的数独算法.分享给大家供大家参考,具体如下: 判断一个 9x9 的数独是否有效.只需要根据以下规则,验证已经填入的数字是否有效即可. 1.数字 1-9 在每一行只能出现一次. 2.数字 1-9 在每一列只能出现一次. 3.数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次. 数独部分空格内已填入了数字,空白格用 '.' 表示. 示例 1: 输入: [ ["5","3",".",".",

  • Javascript 实现的数独解题算法网页实例

    1)当我们拿到一个题目时,首先会根据已经知道的条件,进行数据的初步整理和分析. 相当于填写出9宫格里,所有的"确定项",以及标记"可能选项". function refreshStat() 2)此后,思考会进入 猜测/验证 的循环阶段. 在9宫格中,可以对于"可能选项"进行尝试,验证是否违背现有条件. 每一个新的分支,最后的结果无非是两种,答案/出错. 复制代码 代码如下: while(true){                    var

  • 利用JavaScript做数独的完整实现过程

    目录 前言 怎么解数独 填第一个格子 填第二个格子 填第三个格子 填第九个格子 综上所述 通过代码来实现 动态展示做题过程 九宫格样式 做题逻辑 总结 前言 最近看到老婆天天在手机上玩数独,突然想起 N 年前刷 LeetCode 的时候,有个类似的算法题(37.解数独),是不是可以把这个算法进行可视化. 说干就干,经过一个小时的实践,最终效果如下: 怎么解数独 解数独之前,我们先了解一下数独的规则: 数字 1-9 在每一行只能出现一次. 数字 1-9 在每一列只能出现一次. 数字 1-9 在每一

  • 如何利用javascript做简单的算法

    目录 1 问题 2 方法 3 实验结果与讨论 1 问题 众所周知,无论是Pycharm或是IDLE.java都可以计算简单的算法,比如加减乘除.然而在Hbuilder中,javascript也可以用来计算数值的加减乘除. 比如,我们计算:假设 y=5,计算 x=y+2,并显示结果. 2 方法 首先利用<p></p>标签写算法题题目.然后利用<button></button>标签创造一个事件,其中标签里面onclick后面的命名一定要加().再然后写一个<

  • 利用JavaScript的%做隔行换色的实例

    如下所示: <html> <head> <meta charset="utf-8"> <title>无标题文档</title> <style type="text/css"> li { list-style-type: none; width: 300px; height: 30px; } </style> </head> <body> <ul>

  • 使用纯前端JavaScript实现Excel导入导出方法过程详解

    公司最近要为某国企做一个**统计和管理系统, 具体要求包含 Excel导入导出根据导入的数据进行展示报表图表展示(包括柱状图,折线图,饼图),而且还要求要有动画效果,扁平化风格Excel导出,并要提供客户端来管理Excel 文件... 要求真多! 现在总算是完成了,于是将我的经验分析出来. 在整个项目架构中,首先就要解决Excel导入的问题. 由于公司没有自己的框架做Excel IO,就只有通过其他渠道了. 嗯,我在github上找到了一个开源库xlsx,通过npm方式来安装. npm inst

  • 利用 JavaScript 构建命令行应用

    目录 1.安装 node 2.安装 Commander.js 3. JavaScript 代码中添加一个库 4.JavaScript 中的选项解析 5.访问命令行数据 6.运行应用 7.选项解析 前言: JavaScript 是一种为 Web 开发的语言,但它的用处已经远远超出了互联网的范畴.由于 Node.js 和 Electron 这样的项目,JavaScript 既是一种通用的脚本语言,也是一种浏览器组件.有专门设计的 JavaScript 库来构建命令行界面.是的,你可以在你的终端中运行

  • 前端进阶之教你利用javascript存储函数

    目录 前言 背景介绍 实现方案思考 js存储函数方案设计 最后 总结 前言 任何一家Saas企业都需要有自己的低代码平台.在可视化低代码的前端研发过程中, 发现了很多有意思的技术需求, 在解决这些需求的过程中, 往往也会给自己带来很多收获, 今天就来分享一下在研发Dooring过程中遇到的前端技术问题--javascript函数存储. 背景介绍 我们都知道要想搭建一个前端页面基本需要如下3个要素: 元素(UI) 数据(Data) 事件/交互(Event) 在 数据驱动视图 的时代, 这三个要素的

  • 如何利用JavaScript 实现继承

    目录 一.背景简介 二.原型对象和对象的关系 二.使用 prototype 和 proto 实现继承 三.使用prototype和proto实现继承 四.通过原型链访问对象的方法和属性 五.其他方式实现继承 七.总结 一.背景简介 JavaScript 在编程语言界是个特殊种类,它和其他编程语言很不一样,JavaScript 可以在运行的时候动态地改变某个变量的类型. 比如你永远也没法想到像isTimeout这样一个变量可以存在多少种类型,除了布尔值true和false,它还可能是undefin

  • 利用JavaScript在网页实现八数码启发式A*算法动画效果

    最近人工智能课老师布置了一个八数码实验,网上看到很多八数码的启发式A*算法,但是大多数都是利用C或者C++在控制台实现的,于是我用js在网页中做了一个类似的. 首先八数码就是一个九宫格,其中有一个空格,其他八个对应数字1-8, 移动空格,使得最后状态为有序,如下图 启发式算法是指在求解时,利用启发函数将不符合规则的解节点去掉,从而缩小问题的解空间. A*算法是利用评价函数的启发式算法,在本例中,利用当前节点状态与最终节点状态所不同的格子数来评估节点的优劣,将优越节点储存并在之后展开,将劣质节点抛

  • 利用JavaScript如何查询某个值是否数组内

    本文主要给大家介绍了关于利用JavaScript查询某个值是否数组内的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍: 问题 > var b = ["aa", "bb"] > "aa" in b 我要查询字符串aa是否在数组里面,in可行么? in 首选说in操作符 用过python的都想是不是可以用in,可惜不能用,先看看python的效果: >>> a = ["aa"

  • JavaScript中new运算符的实现过程解析

    这篇文章主要介绍了JavaScript中new运算符的实现过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 废话不多说直接进入正题,首先我们需要先知道new运算符到底做了哪些事情,再来模拟它实现这一功能. 1. 建立一个空的Object对象: 2. 把这个空对象用__proto__链接到原型 3. 用apply绑定对象的this指向 4. 返回新的对象 知道了new的具体过程之后,我们就可以来试一下用代码实现这一过程. // 传参 New

随机推荐