0.摘要
小时候在报纸上玩过数独,那时候觉得很难,前几天在leetcode上遇到了这个题,挺有意思于是记录下来
一般一道数独题,就像他给的例子这样,9*9的格子,满足 行,列 ,宫均取1-9的数,切互不相同。
那一般正常人的思路会去一点一点的推理,至少我小时候就是这么玩的,具体来说,好比 r7c9(第7行,第9列)的空格,我会找第7行有『6,2,8』,第9列有『3,1,6,5,9』,第9宫有『2,8,5,7,9』,这些的并集就是『1,2,3,5,6,7,8,9』,哦那么空格是4。就这么一点点继续往下推理。
1.余数法
他给的函数接口是这样
void solveSudoku(vector<vector<char>>& board){}
然后我就照着我小时候的思路写了一个版本
1 void solveSudoku(vector<vector<char>>& board) { 2 if(board.size()!=9||board[0].size()!=9) 3 return; 4 vector<vector<vector<bool>>> ex(9,vector<vector<bool>>(9,vector<bool>(9,false))); 5 vector<vector<int>> count(9,vector<int>(9,0)); 6 queue<int> se; 7 for(int i=0;i<9;i++) 8 { 9 for(int j=0;j<9;j++) 10 if(board[i][j]==‘.‘) 11 { 12 int block_i = i/3; 13 int block_j = j/3; 14 for(int c=0;c<9;c++) 15 { 16 if(board[i][c]!=‘.‘&&!ex[i][j][board[i][c]-‘1‘]) 17 { 18 ex[i][j][board[i][c]-‘1‘] = true; 19 count[i][j]++; 20 } 21 if(board[c][j]!=‘.‘&&!ex[i][j][board[c][j]-‘1‘]) 22 { 23 ex[i][j][board[c][j]-‘1‘] = true; 24 count[i][j]++; 25 } 26 int ii = block_i*3 + c/3; 27 int jj = block_j*3 + c%3; 28 if(board[ii][jj]!=‘.‘&&!ex[i][j][board[ii][jj]-‘1‘]) 29 { 30 ex[i][j][board[ii][jj]-‘1‘] = true; 31 count[i][j]++; 32 } 33 } 34 if(count[i][j]==8) 35 se.push(i*9+j); 36 } 37 } 38 while(!se.empty()) 39 { 40 int cur = se.front(); 41 se.pop(); 42 int i = cur/9; 43 int j = cur%9; 44 int block_i = i/3; 45 int block_j = j/3; 46 for(int c=0;c<9;c++) 47 if(!ex[i][j][c]) 48 { 49 board[i][j] = c + ‘1‘; 50 break; 51 } 52 for(int c=0;c<9;c++) 53 { 54 if(board[i][c]==‘.‘&&!ex[i][c][board[i][j]-‘1‘]) 55 { 56 ex[i][c][board[i][j]-‘1‘] = true; 57 count[i][c]++; 58 if(count[i][c]==8) 59 se.push(i*9+c); 60 } 61 if(board[c][j]==‘.‘&&!ex[c][j][board[i][j]-‘1‘]) 62 { 63 ex[c][j][board[i][j]-‘1‘] = true; 64 count[c][j]++; 65 if(count[c][j]==8) 66 se.push(c*9+j); 67 } 68 int ii = block_i*3 + c/3; 69 int jj = block_j*3 + c%3; 70 if(board[ii][jj]==‘.‘&&!ex[ii][jj][board[i][j]-‘1‘]) 71 { 72 ex[ii][jj][board[i][j]-‘1‘] = true; 73 count[ii][jj]++; 74 if(count[ii][jj]==8) 75 se.push(ii*9+jj); 76 } 77 } 78 } 79 }
这里ex是9*9*9的数组,对于非空格的位置ex没有意义,对于ricj的空格(例如r7c9的空格),ex[i][j][9]是一个bool[9]的数组,分别代表跟ricj相关的20个格子(行列宫一共20格)是否包含x {x=1...9};如果包含x,那么ex[i][j][x-1]就是true(例如ex[7][9][0,1,2,4,5,6,7,8]为true,ex[7][9][3]为false),同时为了方便建立一个count[9][9]记录true的个数,count[i][j]记录ex[i][j]中true的个数,一旦count[i][j]==8,那么这个格子就可以推理出来。
那么刚开始先对整个数组扫描一遍,分别记录一遍ex和count,找到那些count==8的,放入一个队列。然后获得队列的对首ricj,把他的值填入(例如r7c9填”4“),同时找到与r7c9相关20个格子中是空格的位置,更新他们的ex和count(例如r1c9的ex[0][8][4-1]改为true),把count==8的push到对尾,如此往复,直到队列为空。
我当时的想法时队列空了应该就能解出来了吧。。。于是submit了,结果过了两个case,还有的case报错了。。。
咦?没解完。。解了的都对了。。我把剩下的手抄了一下,发现确实解不了,不是程序问题,原来我小时候一直解不出来是有原因的,不是我眼神不好,方法有问题。遂百度了一下,原来我的方法叫做”余数法“。余数法求解不了所有的数独问题,难的需要假设来推到出矛盾。但怎么假设好呢,也百度了一下。
2.递归+回溯
网上说的最多的方法,主要还是递归+回溯 暴力求解。
1 int row[9][9] ; 2 int col[9][9] ; 3 int block[9][9] ; 4 5 void solveSudoku(vector<vector<char>>& board) { 6 if(board.size()!=9||board[0].size()!=9) 7 return; 8 memset(row, 0, sizeof(row)); 9 memset(col, 0, sizeof(col)); 10 memset(block, 0, sizeof(block)); 11 //memset(ex, 0, sizeof(ex)); 12 //memset(count, 0, sizeof(count)); 13 //推导 14 //forward(board); 15 //假设 16 for(int i=0;i<9;i++) 17 for(int j=0;j<9;j++) 18 if(board[i][j]!=‘.‘) 19 { 20 row[i][board[i][j]-‘1‘]=1; 21 col[j][board[i][j]-‘1‘]=1; 22 block[(i/3)*3+j/3][board[i][j]-‘1‘]=1; 23 } 24 assume(board,0,0); 25 } 26 27 bool assume(vector<vector<char>>& board,int i,int j) 28 { 29 if(i==9) 30 return true; 31 if(board[i][j] != ‘.‘) 32 return assume(board,i+(j+1)/9,(j+1)%9); 33 else 34 for(int c=0;c<9;c++) 35 { 36 if(!row[i][c]&&!col[j][c]&&!block[i/3*3+j/3][c]) 37 { 38 board[i][j] = c+‘1‘; 39 row[i][c] = col[j][c] = block[i/3*3+j/3][c] = 1; 40 if(assume(board,i+(j+1)/9,(j+1)%9)) 41 return true; 42 board[i][j] = ‘.‘; 43 row[i][c] = col[j][c] = block[i/3*3+j/3][c] = 0; 44 } 45 } 46 return false; 47 }
这种方法是从另外一个角度记录当前的数独数组的情况,维持3个bool类型的数组row[9][9],col[9][9],block[9][9],这里为了初始化memset方便设成了int型。row[i][j]的含义是第i行是否含有j(例如初始时r[1-1][5-1]为真,r[1-1][2-1]为假,第一行有5没有2),col,block同理。assume是递归函数,每次遇到空格就对他从i = 1开始假设,如果他所在的行,列,宫都没有i那就设他为i,继续递归往后填写,遇到矛盾(某个空格不能取1-9之间任何数)就返回。这样做就是所谓的暴力求解,这么做肯定是没问题了,可以求解出正确结果。提交,ac了,4ms,打败了83%的人。。。
3.预处理+递归+回溯
但是我想,递归的复杂度和剩余格子的总数有指数关系,直接递归有点浪费时间,何尝不先用余数法给”预处理“一下呢,减少递归次数。。于是,两种方法一起,哦了
1 int row[9][9] ; 2 int col[9][9] ; 3 int block[9][9] ; 4 void solveSudoku(vector<vector<char>>& board) { 5 if(board.size()!=9||board[0].size()!=9) 6 return; 7 memset(row, 0, sizeof(row)); 8 memset(col, 0, sizeof(col)); 9 memset(block, 0, sizeof(block)); 10 //推导 derivation 对于九宫格中可能性唯一的数 直接求解 减少递归次数 11 //余数法 12 forward(board); 13 for(int i=0;i<9;i++) 14 for(int j=0;j<9;j++) 15 if(board[i][j]!=‘.‘) 16 { 17 row[i][board[i][j]-‘1‘]=1; 18 col[j][board[i][j]-‘1‘]=1; 19 block[(i/3)*3+j/3][board[i][j]-‘1‘]=1; 20 } 21 //假设 assume 对于可能性不唯一的数 递归假设求解 22 assume(board,0,0); 23 } 24 25 bool assume(vector<vector<char>>& board,int i,int j) 26 { 27 if(i==9) 28 return true; 29 if(board[i][j] != ‘.‘) 30 return assume(board,i+(j+1)/9,(j+1)%9); 31 else 32 for(int c=0;c<9;c++) 33 { 34 if(!row[i][c]&&!col[j][c]&&!block[i/3*3+j/3][c]) 35 { 36 board[i][j] = c+‘1‘; 37 row[i][c] = col[j][c] = block[i/3*3+j/3][c] = 1; 38 if(assume(board,i+(j+1)/9,(j+1)%9)) 39 return true; 40 board[i][j] = ‘.‘; 41 row[i][c] = col[j][c] = block[i/3*3+j/3][c] = 0; 42 } 43 } 44 return false; 45 } 46 void forward(vector<vector<char>>& board) 47 { 48 bool ex[9][9][9] ; 49 int count[9][9] ; 50 memset(ex, 0, sizeof(ex)); 51 memset(count, 0, sizeof(count)); 52 queue<int> se; 53 //求解所有可能性唯一的 54 //get all results with only one possible answer 55 for(int i=0;i<9;i++) 56 for(int j=0;j<9;j++) 57 if(board[i][j]==‘.‘) 58 { 59 for(int c=0;c<9;c++) 60 { 61 if(board[i][c]!=‘.‘&&!ex[i][j][board[i][c]-‘1‘]) 62 { 63 ex[i][j][board[i][c]-‘1‘] = true; 64 count[i][j]++; 65 } 66 if(board[c][j]!=‘.‘&&!ex[i][j][board[c][j]-‘1‘]) 67 { 68 ex[i][j][board[c][j]-‘1‘] = true; 69 count[i][j]++; 70 } 71 int ii = (i/3)*3 + c/3; 72 int jj = (j/3)*3 + c%3; 73 if(board[ii][jj]!=‘.‘&&!ex[i][j][board[ii][jj]-‘1‘]) 74 { 75 ex[i][j][board[ii][jj]-‘1‘] = true; 76 count[i][j]++; 77 } 78 } 79 //答案唯一的 push到队列 80 if(count[i][j]==8) 81 se.push(i*9+j); 82 } 83 while(!se.empty()) 84 { 85 int cur = se.front(); 86 se.pop(); 87 int i = cur/9; 88 int j = cur%9; 89 for(int c=0;c<9;c++) 90 if(!ex[i][j][c]) 91 { 92 board[i][j] = c + ‘1‘; 93 break; 94 } 95 for(int c=0;c<9;c++) 96 { 97 if(board[i][c]==‘.‘&&!ex[i][c][board[i][j]-‘1‘]) 98 { 99 ex[i][c][board[i][j]-‘1‘] = true; 100 count[i][c]++; 101 if(count[i][c]==8) 102 se.push(i*9+c); 103 } 104 if(board[c][j]==‘.‘&&!ex[c][j][board[i][j]-‘1‘]) 105 { 106 ex[c][j][board[i][j]-‘1‘] = true; 107 count[c][j]++; 108 if(count[c][j]==8) 109 se.push(c*9+j); 110 } 111 int ii = (i/3)*3 + c/3; 112 int jj = (j/3)*3 + c%3; 113 if(board[ii][jj]==‘.‘&&!ex[ii][jj][board[i][j]-‘1‘]) 114 { 115 ex[ii][jj][board[i][j]-‘1‘] = true; 116 count[ii][jj]++; 117 if(count[ii][jj]==8) 118 se.push(ii*9+jj); 119 } 120 } 121 } 122 }
0ms,ac。总结就是,余数法推到减少假设次数 + 递归假设求解子问题 。因为问题规模固定是9*9,因此损失的空间复杂度也能接受。
写了这个着实激发了我很大的兴趣,于是后面又写了生成题库的模块,图形界面的模块。。。