JavaScript解数独

291 阅读5分钟

「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」。

简介

数独(shù dú)是源自18世纪瑞士的一种数学游戏。是一种运用纸、笔进行演算的逻辑游戏。玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行、每一列、每一个粗线宫(3*3)内的数字均含1-9,不重复 [1] 。

数独盘面是个九宫,每一宫又分为九个小格。在这八十一格中给出一定的已知数字和解题条件,利用逻辑和推理,在其他的空格上填入1-9的数字。使1-9每个数字在每一行、每一列和每一宫中都只出现一次,所以又称“九宫格”。

背景

数独起源于18世纪初瑞士数学家欧拉等人研究的拉丁方阵(Latin Square)。19世纪80年代,一位美国的退休建筑师格昂斯(Howard Garns)根据这种拉丁方阵发明了一种填数趣味游戏,这就是数独的雏形。20世纪70年代,人们在美国纽约的一本益智杂志《Math Puzzles and Logic Problems》上发现了这个游戏,当时被称为填数字(Number Place),这也是公认的数独最早的见报版本。1984年一位日本学者将其介绍到了日本,发表在Nikoli公司的一本游戏杂志上,当时起名为“数字は独身に限る”(すうじはどくしんにかぎる),就改名为“数独”(すうどく),其中“数”(すう)是数字的意思,“独”(どく)是唯一的意思。后来一位前任香港高等法院的新西兰籍法官高乐德(Wayne Gould)在1997年3月到日本东京旅游时,无意中发现了 [2] 。他首先在英国的《泰晤士报》上发表,不久其他报纸也发表,很快便风靡全英国,之后他用了6年时间编写了电脑程序,并将它放在网站上(这个网站也就是著名的数独玩家论坛),后来因一些原因,网站被关闭,幸好数独大师Glenn Fowler恢复了数据,玩家论坛有了新处所。在90年代国内就有部分的益智类书籍开始刊登,南海出版社在2005年出版了《数独1-2》,随后日本著名数独制题人西尾彻也的《数独挑战》也由辽宁教育出版社出版。《北京晚报》、《扬子晚报》、《羊城晚报》、《新民晚报》、《成都商报》等等报纸媒体也先后刊登了数独游戏。

问题描述

编写一个程序,通过已填充的空格来解决数独问题。

一个数独的解法需遵循如下规则:

  • 数字 1-9 在每一行只能出现一次。
  • 数字 1-9 在每一列只能出现一次。
  • 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。

空白格用 '.' 表示。

在这里插入图片描述

一个数独。

在这里插入图片描述

答案被标成红色。

Note:

  • 给定的数独序列只包含数字 1-9 和字符 '.' 。
  • 你可以假设给定的数独只有唯一解。
  • 给定数独永远是 9x9 形式的。

题目分析

看到题目我们很容易地想到应该要用回溯算法来解答此题,对此我们需要一个检查位置的函数来判断当前位置能否填写某一数字。之后遍历每个位置,试着将1~9填入每个位置,直到填满整个9宫格结束遍历。最容易想到的方法是用一个数组记录每个数字是否出现。由于我们可以填写的数字范围为 [1, 9],而数组的下标从 0 开始,因此在存储时,我们使用一个长度为 9 的布尔类型的数组,其中 i 个元素的值为 true,当且仅当数字 i+1 出现过。

在这里插入图片描述

小九宫格下标如上图所示,主要步骤如下

步骤

  • 数独首先行,列,还有 3*3 的小九宫格内数字是 1~9 不能重复。
  • 声明布尔数组,表明行列中某个数字是否被使用了, 被用过标记为 true,没用过标记为 false。
  • 初始化布尔数组,表明哪些数字已经被使用过了。
  • 尝试去填充数组,只要行,列, 还有 3*3 的方格内 出现已经被使用过的数字,我们就不填充,否则尝试填充。
  • 如果填充失败,那么我们需要回溯。将原来尝试填充的地方改回来。
  • 递归直到数独被填充完成。

代码

var columns = [],//保存每一行的数字
    rows = [],//保存每一列的数字
    grids = [],//保存每一个九宫格的数字
    f = 0;//结束标记// 初始化,遍历已有填充值
var init = function(board){
  for (let i = 0; i < 9; i++) {
    for (let j = 0; j < 9; j++) {
      // 获取值
      const value = board[i][j];
      // 先判断非 . 元素
      if (value !== '.') {
          rows[i].push(value);
          columns[j].push(value);
          //九宫格下标
          const gridIndex = Math.floor(i / 3) * 3 + Math.floor(j / 3);
          grids[gridIndex].push(value);
        } 
      }
    }
};
//检查能否填入该位置
var check = function(i,j,board,value){
    const gridIndex = Math.floor(i / 3) * 3 + Math.floor(j / 3); // 对应的盒子
    if (rows[i].includes(value) || columns[j].includes(value) || grids[gridIndex].includes(value)) {
        return false;
    }
    return true;
};
//遍历填充数独
var fillBoard = function(i,j,board){
    //已填满
    if(f == 1 || i > 8) {
        return ;
    }
    //需要填充
    if(board[i][j] == '.'){
        //遍历9个数字
        for(let num = 1; num < 10; num++){
            if(f == 0 && check(i,j,board,num.toString())){
                rows[i].push(num.toString());//更新列
                columns[j].push(num.toString());//更新行
                const gridIndex = Math.floor(i / 3) * 3 + Math.floor(j / 3);
                grids[gridIndex].push(num.toString());//更新小九宫格
                board[i][j] = num.toString();//填入数字
                if(i == 8 && j == 8) {//已填满
                    f = 1;
                    return;
                }else if(j == 8) fillBoard(i + 1,0,board);//换行
                else fillBoard(i,j+1,board);//换列
                 if(f == 0){//回溯
                    rows[i].pop();//更新列
                    columns[j].pop();//更新行
                    grids[gridIndex].pop();//更新小九宫格
                    board[i][j] = '.';//更新大九宫格
                }
            }
        }
    }else if(i == 8 && j == 8) {//遍历结束
        f = 1;
        return;
    }else if(j == 8) fillBoard(i + 1,0,board);//换行
    else fillBoard(i,j+1,board);//换列
    return board;
};
var solveSudoku = function(board) {
    f = 0;
    for(let i = 0; i < 9; i++){
        columns[i] = [];
        rows[i] = [];
        grids[i] = [];
    }
    init(board);  
    board = fillBoard(0,0,board);
};