求解数独难题, Sudoku问题(回溯)

时间:2021-09-30 20:44:48

Introduction :

求解数独难题, Sudoku问题(回溯)

标准的数独游戏是在一个 9 X 9 的棋盘上填写 1 – 9 这 9 个数字,规则是这样的:

  • 棋盘分成上图所示的 9 个区域(不同颜色做背景标出,每个区域是 3 X 3 的子棋盘),在每个子棋盘中填充 1 – 9 且不允许重复 ,下面简称块重复
  • 每一行不许有重复值 ,下面简称行重复
  • 每一列不许有重复值 ,下面简称列重复

如上红色框出的子区域中的亮黄色格子只能填 8。

扩展阅读:http://en.wikipedia.org/wiki/Sudoku


Goals :

  1. 随机生成数独问题,如果做成一个数独小游戏的话可以按游戏难易程度生成,比如有简单、中等、困难三个程度
  2. 求解数独问题,根据生成的数独问题或者用户输入的数独问题求解出所有答案或者求解出不超过MAX个答案,因为某个问题可能含有上十万个解,用MAX约束找到MAX个答案后就不再找其它的答案

Idea :

有一种求解数独问题的方案是“候选数字法”,就是在待填充的格子中填写不会造成行重复、列重复、块重复的数字,有的时候存在多个这样的数字,那么我们可以随机选取一个,如果待填充的格子中填写任何一个数字都会造成某种重复的发生,则说明这个问题没有解,也就是这不是一个数独问题。如上图中红色框出区域的下面一个区域的红色格子中可以填写的数字为 4、8、9,那么它的候选数字就是4、8、9,你可以随机选一个填入。当所有的格子填充完后数独问题就解决了。

根据上述的这种候选数字法,我们可以用它来生成数独问题以及求解数独问题。

关于生成数独问题:

循环遍历 9 X 9 的数独格子,在遍历到的当前格子的候选数字中随机选取一个数字填入,如果当前格子没有了候选数字则清空所有已经填好的格子,重新再来。这是一个最简单的也是最容易理解的概率方法了。伪码如下:

   1: int[][] sudoku = new int[9][9]; //数独棋盘
   2: while(true) {
   3: label:
   4:     clearSudoku(); //清空所有已填数字, 每个格子置 0
   5:     for(int row = 0; row < 9; row++) {
   6:         for(int col = 0; col < 9; col++) {
   7:             //getCandidates(row, col) 获取当前格子的候选数字
   8:             //randomCandidate() 随机选取一个候选数字
   9:             if(getCandidates(row, col) != NULL) //如果有候选数字
  10:                 sudoku[row][col] = randomCandidate(getCandidates(row, col));
  11:             else
  12:                 goto label; //跳转到label那里清空格子重新来过
  13:         }
  14:     }
  15: }

关于求解数独问题:

同上, 不过这次不是采用随机算法,因为随机算法没法保证把所有的解都求出。利用回溯法把所有可能的情况都遍历,那么就可以求出所有的解。我们考虑下面的一个实例:

求解数独难题, Sudoku问题(回溯)

初始时,sudoku[0][0]格子的候选数字为2, 3,5, 6,如图标出,sudoku[4][0]的为2,4,5,其它如图。那么我们在填充sudoku[0][0]的时候不能按照随机来了,因为我们需要把所有的情况遍历的话,需要按一定顺序来,也就是从2开始来,因为一旦某个格子填充了一个候选数字后,其它格子的候选数字会发生变化,假设sudoku[0][0]填充了2,那么与sudoku[0][0]在同一行、同一列、同一块内的格子的候选数字就不能再有2了,sudoku[4][0]、sudoku[6][0]等都应该将原来含有的2给删掉。

我们遍历着填写候选数字,当遇到某个格子的候选数字不存在的时候,我们应该回溯了,我们回到上一格,填写另一个候选数字。比如上图,我们按照这样的顺序来:

  1. sudoku[0][0]填写2,sudoku[4][0]候选为(4,5),sudoku[6][0]候选为(3, 5),sudoku[7][0]候选为(4, 5, 6),sudoku[8][0]候选为(5, 6);
  2. sudoku[4][0]填写4,sudoku[6][0]候选为(3, 5),sudoku[7][0]候选为(5, 6),sudoku[8][0]候选为(5, 6);
  3. sudoku[6][0]填写5,sudoku[7][0]候选为(6),sudoku[8][0]候选为(6);
  4. 那么sudoku[7][0]填了6之后sudoku[8][0]就没有候选数字可填了

至于如何遍历所有的情况,我们可以这样来:

把候选数字都按大小从小到大排序,用一个栈记录已经选取过的候选数字在这个序列中的序号,每一次选取候选数字就入栈,每一次回溯就出栈。另外需要注意的是每一次改变格子数字的操作都需要更新候选数字。伪码如下(注意,这只是表达思想,代码在如果按执行顺序来是有误的):

   1: Stack s;
   2: int idx;
   3: while(true) {
   4:     if(candidates[row][col] != NULL) {
   5:         //当前格子填写第idx个候选数字, idx = 0,1,2,3...
   6:         sudoku[row][col] = candidates[row][col](idx);
   7:         //如果所有的格子都填了,那么就把这个解保存起来,然后回溯
   8:         if(allBlanked()) {
   9:             solutions.add();
  10:             backtrace(row, col);
  11:         }
  12:         //idx入栈以记录已经填过的候选数字
  13:         s.push(idx);
  14:         //更新候选数字
  15:         updateCandidates();
  16:         //填写下一行
  17:         next(row, col);
  18:     }
  19:     else {//如果没有了候选数字就回到上一格填过的位置
  20:         backtrace(row, col);
  21:         //回溯到原点就结束
  22:         if(s.isEmpty()) {
  23:             end;
  24:         }
  25:         //获取上一格填写的数字的序号,这次应该填写下一个候选数字了,即序号加1
  26:         idx = s.pop() + 1;
  27:     }
  28: }
转自:http://ggicci.blog.163.com/blog/static/21036409620127741430397/