题目:数独
描述:请编写一个程序,给数独中的剩余的空格填写上数字
空格用字符'.'表示
解法一:
思路分析:首先我们通过该题目了解一下什么是数独,数独是十八世纪在瑞士产生的一种小游戏,是通过不断的演算推导出来的逻辑游戏,在上述中,属于9行9列的格子,且被分为9个3*3的方块,该方块内的数字取值范围为1-9,玩家通过在剩余的空白格子中填入数字,数字需要满足的条件为数字1-9在每一行每一列只能出现一次,且数字1-9在每一个粗实线分隔的3*3宫内只能出现一次。
我们需要做的就是,找出当前可能存在的各种解法,不断地填入可能的数字,如果通过演算得出的解法错误就将该结果撤回,否则的话,就已知尝试不同的解法,通过上述描述,我们可以想到的首先是递归的方法,通过尝试不同的可能的解,向上返回所有的尝试结果,可能的结果只有两种,一种为成功,一种为失败,具体采用的递归方法为:
1、首先推测出每一个空白的格子中可能存在的数字
2、找到可供选择的最小的那个格子
3、如果有可选的数字,填入可选的数字,生成一个数独,否则的话就返回失败的信息
4、不断的通过调用递归函数来处理新的数独
5、不断的填入新的数字进行判断,当成功则返回成功的信息,当失败则重新进行递归判断。
6、每成功一次就向上一层返回成功的信息
因为空白格子有一定的数量,所以我们使用1-9来进行记录,采用unsigned int类型进行记录从右往左数的第K为代表的数字K,使用1定位相应的位置,使用或和与运算进行相关的操作,对于i行j列的小方格,使用i / 3 * 3 + j / 3即可以得出相应3 * 3方块的下标。
实例分析:在一个实例分析中我们采用表格进行判断,简化操作
5 | 3 |
|
| 7 |
|
|
|
|
6 |
|
| 1 | 9 | 5 |
|
|
|
| 9 | 8 |
|
|
|
| 6 |
|
8 |
|
|
| 6 |
|
|
| 3 |
4 |
|
| 8 |
| 3 |
|
| 1 |
7 |
|
|
| 2 |
|
|
| 6 |
| 6 |
|
|
|
| 2 | 8 |
|
|
|
| 4 | 1 | 9 |
|
| 5 |
|
|
|
| 8 |
|
| 7 | 9 |
5 | 3 | 1,2,4 |
6 |
2,7 | 2,4,7 |
1,2 | 9 | 8 |
5 | 3 | 4 |
6 | 2 | 7 |
1 | 9 | 8 |
5 | 3 | 4 |
6 | 7 | 2 |
1 | 9 | 8 |
具体C++代码为:
class Solution { public: void solveSudoku(vector<vector<char>>& board) { unsigned int pos_r[9];//每一行剩下的可选数字 unsigned int pos_l[9];//每一列剩下的可选数字 unsigned int pos_b[9];//每一方块剩下的可选数字 //九个数字均可选 for (int i = 0; i < 9; i++){ pos_r[i] = 0b111111111; pos_l[i] = 0b111111111; pos_b[i] = 0b111111111; } for (int i = 0; i < 9; i++){ for (int j = 0; j < 9; j++){ if(board[i][j] != '.' ){//出现了新的数字,空白格用点号表示 unsigned int bit = 1 << (board[i][j] - '1'); pos_r[i] &= ~bit;//把相应的位置为0 pos_l[j] &= ~bit; pos_b[i/3*3+j/3] &= ~bit; } } } slove(board, pos_r, pos_l, pos_b);//调用函数 } bool slove(vector<vector<char>>& board, unsigned int *pos_r, unsigned int *pos_l, unsigned int *pos_b){ int min_choice = 10; int a = 0, b = 0; unsigned int pos_bit = 0; for (int i = 0; i < 9; i++){ for (int j = 0; j < 9; j++){ if(board[i][j] == '.' ){ //该位置没有填 unsigned int possible = pos_r[i] & pos_l[j] & pos_b[i/3*3+j/3];//计算出同时满足三个条件的数字 if (possible == 0) return false;//无解 int choice = __builtin_popcount(possible);//计算可能的数字有多少(1的位数) if (choice < min_choice){//找到可填入数字最少的 min_choice = choice; a = i; b = j; pos_bit = possible; } } } } if (min_choice == 10) return true; //全部填完了,返回 //pos_bit 记录可能的数字 while(pos_bit){ unsigned int test_bit = 1 << __builtin_ctz(pos_bit);//从最右边的位开始(选最小的数字) board[a][b] = __builtin_ctz(pos_bit) + '1';//填入数字 pos_r[a] &= ~test_bit; pos_l[b] &= ~test_bit; pos_b[a/3*3+b/3] &= ~test_bit; if (slove(board, pos_r, pos_l, pos_b)){ return true;//成功 返回 } //失败,撤回后继续试 board[a][b] = '.'; pos_r[a] |= test_bit; pos_l[b] |= test_bit; pos_b[a/3*3+b/3] |= test_bit; //清除试过的位 pos_bit &= ~test_bit; } return false;//找不到解 } };
因为要遍历每一行每一列的元素来进行判断,所以其时间复杂度为O(N^2),N为i行或j列,用三个数组表示可选择的数字,空间复杂度为O(N * i * j)。
解法二:
思路分析:上述代码较为复杂,可以采用暴力法代码,主要遵循三个基本原则每一行只能出现1~9这九个数字,并且不能重复,每一列只能出现1~9这九个数字,并且不能重复。每一个九宫格里面只能出现1~9这九个数字,不能重复,可直接通过暴力解决。
具体Java代码为:
public class Solution { public void solveSudoku(char[][] board) { if(board == null || board.length == 0)//特殊情况 return; solve(board); } public boolean solve(char[][] board){ for(int i = 0; i < board.length; i++){ for(int j = 0; j < board[0].length; j++){ if(board[i][j] == '.'){ for(char c = '1'; c <= '9'; c++){//尝试1-9的情况 if(isValid(board, i, j, c)){ board[i][j] = c; //将c的值放进去 if(solve(board)) return true; //如果解决了返回true else board[i][j] = '.'; //其他的返回. } } return false; } } } return true; } private boolean isValid(char[][] board, int row, int col, char c){ for(int i = 0; i < 9; i++) { if(board[i][col] != '.' && board[i][col] == c)//检查行 return false; if(board[row][i] != '.' && board[row][i] == c)//检查列 return false; if(board[3 * (row / 3) + i / 3][ 3 * (col / 3) + i % 3] != '.' && board[3 * (row / 3) + i / 3][3 * (col / 3) + i % 3] == c)//检查子宫格 return false; } return true; } }
其时间复杂度为O(N^3),因为包括行列的循环和数字1-9的循环,所以最大时间复杂度为O(N^3),空间复杂度也为O(N^3)。