Fancy DSA Fancy DSA
数据结构
算法
LeetCode
  • 关于
  • 导航 (opens new window)
  • 分类
  • 标签
  • 归档
设计模式 (opens new window)
博客 (opens new window)
GitHub (opens new window)

Jonsam NG

想的更多,也要想的更远
数据结构
算法
LeetCode
  • 关于
  • 导航 (opens new window)
  • 分类
  • 标签
  • 归档
设计模式 (opens new window)
博客 (opens new window)
GitHub (opens new window)
  • 开始上手
  • Plan 计划
  • Roadmap 路线
  • 算法简介
  • Sort 排序

  • Search 搜索

  • Recursive 递归

  • Graph 图

  • Tree 树

  • Math 数学

  • Hash 哈希

  • String 字符串

  • BitManipulation 位操纵

  • Backtracking 回溯

    • AllCombinationsOfSizeK [全组合]
    • GeneratePermutations [排列]
    • KnightTour [骑士巡逻]
    • NQueens [N皇后问题]
    • RatInAMaze [迷宫之鼠]
    • Sudoku [数独]
      • 介绍
      • Backtracking
      • 实现
      • 扩展
      • Crook’s Algorithm
      • 参考
    • SumOfSubset [子集之和]
  • DynamicProgramming 动态规划

  • Cache 缓存

  • Array 数组

  • Ciphers 密码学

  • Conversions 转换

  • ProjectEuler 欧拉计划

  • 其他

  • 算法
  • Backtracking 回溯
jonsam
2022-09-26
目录

Sudoku [数独]

# 介绍

数独(日语:数独/すうどく Sūdoku */?)是一种数学逻辑游戏,游戏由 9×9 个格子组成,玩家需要根据格子提供的数字推理出其他格子的数字。游戏设计者会提供最少 17 个数字使得解答谜题只有一个答案。

image

规则:

  • 游戏一般由 9 个 3×3 个的九宫格组成。
  • 每一列的数字均须包含 1~9,不能缺少,也不能重复。
  • 每一宫 (粗黑线围起来的区域,通常是 3*3 的九宫格) 的数字均须包含 1~9,不能缺少,也不能重复。

有几种计算机算法可以在几分之一秒内解决 9×9 的谜题(n=9),但是随着 n 的增加会出现组合爆炸,从而对可以构建、分析和解决 n 增加的 Sudokus 的属性造成限制。

# Backtracking

一些业余爱好者开发了计算机程序,将使用回溯算法来解决数独谜题,这是一种蛮力搜索(brute force)。

image

蛮力算法以某种顺序访问空单元格,按顺序填入数字,或者在发现数字无效时进行回溯。简而言之,一个程序会通过在第一个单元格放置数字 "1" 并检查是否允许它出现在那里来解决一个谜题。如果没有违反规定(检查行、列和框的约束),那么算法就会推进到下一个单元格,并在该单元格中放置一个 "1"。在检查违规情况时,如果发现 "1" 是不允许的,该值就会被推进到 "2"。如果发现一个单元格中的 9 位数字都不允许,那么该算法将该单元格留空,并移回前一个单元格。然后,该单元格中的值将被增加 1。如此反复,直到发现最后一个(第 81 个)单元格中的允许值。

这种方法的优点是:

  • 保证有一个解决方案(只要谜题是有效的)。
  • 解题时间大多与难度无关。
  • 该算法(也就是程序代码)比其他算法更简单,尤其是与确保最难的谜题得到解决的复杂算法相比。

# 实现

# JavaScript

class Sudoku {
  // Sudoku Class to hold the board and related functions
  constructor (board) {
    this.board = board
  }

  findEmptyCell () {
    // Find a empty cell in the board (returns [-1, -1] if all cells are filled)
    for (let i = 0; i < 9; i++) {
      for (let j = 0; j < 9; j++) {
        if (this.board[i][j] === 0) return [i, j]
      }
    }
    return [-1, -1]
  }

  check ([y, x], value) {
    // checks if the value to be added in the board is an acceptable value for the cell

    // checking through the row
    for (let i = 0; i < 9; i++) {
      if (this.board[i][x] === value) return false
    }
    // checking through the column
    for (let i = 0; i < 9; i++) {
      if (this.board[y][i] === value) return false
    }

    // checking through the 3x3 block of the cell
    const secRow = Math.floor(y / 3)
    const secCol = Math.floor(x / 3)
    for (let i = (secRow * 3); i < ((secRow * 3) + 3); i++) {
      for (let j = (secCol * 3); j < ((secCol * 3) + 3); j++) {
        if (y !== i && x !== j && this.board[i][j] === value) return false
      }
    }

    return true
  }

  solve () {
    const [y, x] = this.findEmptyCell()

    // checking if the board is complete
    if (y === -1 && x === -1) return true

    for (let val = 1; val < 10; val++) {
      if (this.check([y, x], val)) {
        this.board[y][x] = val
        if (this.solve()) return true
        // backtracking if the board cannot be solved using current configuration
        this.board[y][x] = 0
      }
    }
    // returning false the board cannot be solved using current configuration
    return false
  }

  getSection (row, [start, end]) {
    return this.board[row].slice(start, end)
  }

  printBoard (output = (...v) => console.log(...v)) {
    // helper function to display board
    for (let i = 0; i < 9; i++) {
      if (i % 3 === 0 && i !== 0) {
        output('- - - - - - - - - - - -')
      }
      output(
        ...this.getSection(i, [0, 3]), ' | ',
        ...this.getSection(i, [3, 6]), ' | ',
        ...this.getSection(i, [6, 9]))
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75

# 扩展

使用 Bit Masks 解决 Sudoku 问题:

对于每一行 / 每一列 / 每一个 Box 都创建一个 Bit Mask,对于网格中的每一个元素,在相应的 Bit Mask 中把位置 ' 值' 的比特设置为 1,进行 O(1)检查。

按照下面的步骤来解决问题:

  • 创建 3 个大小为 N 的数组(行、列、盒各一个)。
  • 盒子的索引从 0 到 8。(为了找到一个元素的盒子索引,我们使用以下公式:行 / 3*3 + 列 / 3)。
  • 首先映射网格的初始值。
  • 每次我们向 / 从网格中添加 / 删除一个元素时,都要把相应的位掩码设置为 1/0。
const N = 9
 
// Bitmasks for each row/column/box
let row = new Array(N), col = new Array(N), box = new Array(N);
let seted = false;
 
// Utility function to find the box index
// of an element at position [i][j] in the grid
function getBox(i,j) {
  return Math.floor(i / 3) * 3 + Math.floor(j / 3);
}
 
// Utility function to check if a number
// is present in the corresponding row/column/box
function isSafe(i,j,number) {
  return !((row[i] >> number) & 1) && !((col[j] >> number) & 1) && !((box[getBox(i,j)] >> number) & 1);
}
 
// Utility function to set the initial values of a Sudoku board
// (map the values in the bitmasks)
function setInitialValues(grid) {
  for (let i = 0; i < N;i++)
      for (let j = 0; j < N; j++)
          row[i] |= 1 << grid[i][j],
          col[j] |= 1 << grid[i][j],
          box[getBox(i, j)] |= 1 << grid[i][j];
}
 
/* Takes a partially filled-in grid and attempts to assign values to all unassigned locations in
such a way to meet the requirements for Sudoku solution (non-duplication across rows, columns, and boxes) */
function SolveSudoku(grid ,i, j) {
  // Set the initial values
  if(!seted){
      seted = true,
      setInitialValues(grid);
  }

  if(i == N - 1 && j == N) return true;
  if(j == N){
      j = 0;
      i++;
  }

  if(grid[i][j]) return SolveSudoku(grid, i, j + 1);

  for (let nr = 1; nr <= N;nr++) {
      if(isSafe(i, j, nr)) {
        /* Assign nr in the current (i, j) position and add nr to each bitmask */
        grid[i][j] = nr;
        row[i] |= 1 << nr;
        col[j] |= 1 << nr;
        box[getBox(i, j)] |= 1 << nr;

        if(SolveSudoku(grid, i,j + 1)) return true;

        // Remove nr from each bitmask and search for another possibility
        row[i] &= ~(1 << nr);
        col[j] &= ~(1 << nr);
        box[getBox(i, j)] &= ~(1 << nr);
      }

      grid[i][j] = 0;
  }

  return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

# Crook’s Algorithm

温斯洛普大学计算机科学教授詹姆斯 - 克鲁克发表了一篇名为《解决数独谜题的纸笔算法 (opens new window)》的论文。在论文中,Crook 提到,如果没有进一步的可能步骤,猜测是必要的。这种猜测并没有加入到如下算法中。因此,当你使用这个算法来解决数独问题时,有可能无法得到答案。

步骤:

  • Markup:列出每个单元格中所有可能的数字。
  • Find singleton:找到如果有一个行、列或框,里只有一个可能的值,成为 singleton。所以你在这个单元格里填上这个数字,然后在受影响的行、列或框里更新标记。
  • Find Preemptive Sets:
  • Eliminate possible numbers outside preemptive sets。

参考:

  • Solve Sudoku more elegantly with Crook’s algorithm in Python | by WY Fok | Towards Data Science (opens new window)
  • wyfok/Solve_Sudoku_with_Crook_algorithm: Apply Sudoku rules and Crook's algorithm to solve Sudoku (opens new window)
  • Mathematics and Sudokus: Solving Algorithms (II) (opens new window)
  • www.ams.org/notices/200904/rtx090400460p.pdf (opens new window)

# 参考

  • Sudoku - Wikiwand (opens new window)
  • Sudoku solving algorithms - Wikiwand (opens new window)
  • Sudoku | Backtracking-7 - GeeksforGeeks (opens new window)
编辑 (opens new window)
上次更新: 2022/10/22, 22:03:41
RatInAMaze [迷宫之鼠]
SumOfSubset [子集之和]

← RatInAMaze [迷宫之鼠] SumOfSubset [子集之和]→

最近更新
01
0-20题解
10-31
02
本章导读
10-31
03
算法与转换:Part1
10-28
更多文章>
Theme by Vdoing | Copyright © 2022-2022 Fancy DSA | Made by Jonsam by ❤
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式