描述
编写一个程序,通过已填充的空格来解决数独问题。
一个数独的解法需遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。
原题地址
胡思乱想
回溯肯定要的,但具体怎么写不知道;答案肯定是要看的,借此文章来加深印象。
思路解法:(回溯法)
- 约束编程:
基本的意思是在放置每个数字时都设置约束。在数独上放置一个数字后立即
排除当前行,列和子方块对该数字的使用。这会传播约束条件并有利于减少需要考虑组合的个数。 - 回溯:
让我们想象一下已经成功放置了几个数字。但是该组合不是最优的并且不能继续放置数字了。该怎么办?回溯。意思是回退,来改变之前放置的数字并且继续尝试。如果还是不行,再次回溯。
生产代码:
对于代码的编写,我们有以下几点需要注意:
- 如何设置约束,使用何种数据结构来保证能够迅速的判断约束情况;
- 回溯函数的编写,其具体思路为何;
- 如何计算方块索引,即根据当前列行的数值获取当前所在方块索引的数值;
我们分别解决上述问题:
- 我们假设在一个空位置上放上数d, 那么我们要判断三个约束,即在当前行是否出现该数,当前列是否出现该数,当前方块是否出现该数。能够立刻想到的是在每次判断的时候都扫描当前行列或方块,是否出现该数,然而这是一种低效的方法,因为很多行、列或者方块会被重复扫描,造成了时间上的浪费。我们使用初始化的技巧,即在一开始将数独扫描一遍,将结果保存,之后每添加一个数或者删除一个数,我们维护这个结果。 可以使用二维数组来达成这样的结果。设置3个二维数组,类型为boolean,其中数组的第一个下标表示当前的位置(一个数组表示行,一个表示列,一个表示方块),第二个下标则表示1~9的数,数组的值表示当前位置有无该数。例如rows[2][8] == true表示在数独的第2行中存在数字8。
- 约束的问题解决了,下面是回溯函数的问题,这涉及到整个方法的核心。
假定定义函数为void backtrack(int row, int col),进行过一次该函数之后,在第row行第col列的位置上会有一个正确的数出现。函数的思路如下:
若该位置为空,从1到9迭代数字d,若某个d不存在约束,将该数放入该位置,并寻找下一个位置;若放置的该数不能导致数独的解决,将此d移除,并尝试下一个d。
若该位置不为空,则直接寻找一个位置。 - 方块索引值 = (行 / 3) * 3 + 列 / 3 (经验获得)
实际上将代码分成以下步骤来编写思路会很清晰:
- 定义约束数组,数独面板二维数组,结束终止标记;
- 编写backtrack函数,先不实现其中要用的部分功能函数;
- 将backtrack中所需要的部分功能函数完成;
- 完成初始化约束数组函数;
具体代码:
class Solution {
//定义数独版
char[][] board = new char[9][9];
//定义约束数组
boolean [][] rows = new boolean[9][10];
boolean [][] cols = new boolean[9][10];
boolean [][] boxs = new boolean[9][10];
//定义是否数独是否完成
boolean isFinished = false;
public void initControls(char [][] board){//初始化约束数组
this.board = board;
for(int i=0; i<9; i++){
for(int j=0; j<9; j++){
if(this.board[i][j]!='.'){
placeNumber(this.board[i][j]-'0',i,j);
}
}
}
}
public boolean couldPlaceNumber(int d, int row, int col){//判断当前位置能否放入该数字
int boxId = (row/3)*3+col/3;
return !rows[row][d]&&!cols[col][d]&&!boxs[boxId][d];
}
public void placeNumber(int d, int row, int col){//放入该数字,同时维护约束数组
int boxId = (row/3)*3+col/3;
rows[row][d] = true;
cols[col][d] = true;
boxs[boxId][d] = true;
board[row][col] = (char)(d + '0');
}
public void removeNumber(int d, int row, int col){//移除该数字,同时维护约束数组
int boxId = (row/3)*3+col/3;
rows[row][d] = false;
cols[col][d] = false;
boxs[boxId][d] = false;
board[row][col] = '.';
}
public void nextPlaceNumber(int row, int col){//寻找下一个位置,同时判断是否完成数独
if(row==8&&col==8) isFinished = true;
else{
if(col+1<9) backtrack(row,col+1);
else backtrack(row+1,0);
}
}
public void backtrack(int row, int col){//核心思路,回溯法
if(board[row][col]=='.'){
for(int i=1; i<=9; i++){
if(couldPlaceNumber(i,row,col)){ //如果当前位置能够放入该数字
placeNumber(i,row,col); //放入该数字
nextPlaceNumber(row,col); //移动到下一个可以到达的位置
if(!isFinished) removeNumber(i,row,col); //若该位置不能够完成数独,移除该数字
}
}
}else{
nextPlaceNumber(row,col); //若当前位置不为空,转移到下一个位置
}
}
public void solveSudoku(char[][] board){
initControls(board); //初始化约束数组
backtrack(0,0);
}
}
心得:
没玩过数独,只知道其中的概念。在考虑很难找到一个通用公式的情况下,应该能用的方法只有回溯了。官方题解的代码非常漂亮,无论是思路还是函数的封装都堪称完美,要学习的远不止其中的核心思路,还有其代码风格。只有把每个函数要做的事情清清楚楚地实现,不多不少,思路清晰,那么就算是递归,也没有什么好怕的。