2
头图

Recently, I saw my wife playing Sudoku on her mobile phone every day. I suddenly remembered that when I brushed LeetCode N years ago, there was a similar algorithm problem ( 37. Sudoku 161356821a99bd). Can this algorithm be visualized?

Just do it, after an hour of practice, the final result is as follows:

How to solve Sudoku

Before solving Sudoku, let's understand the rules of Sudoku:

  1. The number 1-9 can only appear once in each line.
  2. The number 1-9 can only appear once in each column.
  3. The number 061356821a9aeb can only appear once 1-9 3x3 ) separated by a thick solid line.

Next, what we have to do is to fill in a number in each grid, and then determine whether this number violates the regulations.

Fill in the first box

First, fill in 1 in the first grid, and find that there is already a 1 in the first column. At this time, you need to erase the number 1 that was filled in before, and then fill in the grid 061356821a9bb5, and find that the number is in the row, column, and 2 No duplication. Then this box is filled successfully.

Fill in the second box

Let's look at the second grid below. Just like the previous one, try to fill in 061356821a9c2d first, and find that there is no repetition of the numbers in the rows, columns, and 1

Fill in the third box

Let’s take a look at the third box. Because of the first two 1 , 2 , so we directly start with the numbers 3 . After filling in 3 , I found that there was already a 3 in the first row, and then filled in 061356821a9cc8 in the grid, and found that the number 4 4 , but it was still unsuccessful, and then I tried to fill in the number 5 , and finally there was no repetition. The number indicates that the filling is successful.

Keep filling until the ninth box is filled

Follow this idea and fill in the ninth grid. At this time, you will find that the last number 061356821a9d68 conflicts in the 9 And 9 already the last number, there is no way to fill in other numbers here, you can only go back to the previous grid, change the number of the seventh grid from 8 to 061356821a9d6d, and find that there is still a conflict in the 9

At this time, you need to replace the number in the previous grid (the sixth grid). Until there is no conflict, so in this process, not only have to fill in the numbers back, but also look back to see if there are any problems with the previous numbers, and keep trying.

In summary

Solving Sudoku is a constantly trying process. Try the number 1-9 in each grid. If there is a conflict, erase the number until all the grids are filled.

Through code

To reflect the above solution to the code, it needs to be achieved through the recursion + backtracking.

Before writing the code, let's take a look at how to express Sudoku. Here, refer to the title on : 161356821a9e82 37. Sudoku 161356821a9e83.

The previous topic can be represented by a two-dimensional array. There are a total of 9 arrays in the outermost array, representing 9 rows of Sudoku. The 9 characters in each array correspond to the columns of the array, and the unfilled spaces are represented '.'

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'],
]

After we know how to represent the array, let's write the code.

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
}

The above code only implements the part of recursion and backtracking, and there is another isValid is not implemented. This method is mainly to perform a verification in accordance with the rules of Sudoku.

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
}

Through the above code, we can solve a Sudoku.

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) // 输出结果

输出结果

Dynamic display of the problem-making process

With the above theoretical knowledge, we can apply this problem-making process to react, and dynamically display the problem-making process, which is what it looks like in the Gif at the beginning of the article.

Here directly use create-react-app scaffolding to quickly start a project.

npx create-react-app sudoku
cd sudoku

Open App.jsx and start writing code.

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>
    );
  }
}

Jiugongge style

Add a dotted border to each grid, first make it look like a nine-square grid.

.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;
}

You can get a graph like this:

Next, you need to add a solid border to the outer border and each nine-square grid. The specific code is as follows:

/* 第 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;
}

Here you will find that the right borders of the third and sixth columns overlap with the left borders of the fourth and seventh columns a little bit. The bottom borders of the third and sixth rows and the top borders of the fourth and seventh rows will also have this problem. So, we You also need to hide the left borders of the fourth and seventh columns and the bottom borders of the third and sixth rows.

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

Problem-solving logic

After the style is written, you can continue to improve the logic of the question.

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 (……)  }}

Compared with the previous logic, here is just after filling in the blanks of the two-dimensional Sudoku array, calling this.setState to synchronize sudoku state .

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

After calling solveSudoku , it was found that there was no dynamic effect, but the result was synchronized to the view in one step.

This is because setState is a pseudo-asynchronous call. In an event task, all setState will be merged into one time. We need to see the dynamic problem- setState process. We need to put each 061356821aa3a3 operation outside of the event stream. Just put it in setTimeout . For more about setState asynchronous issues, please refer to my previous article: setState a macro task or a micro task in React?

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)}

The final effect is as follows:


Shenfq
4k 声望6.8k 粉丝