0.摘要c++
小時候在報紙上玩過數獨,那時候以爲很難,前幾天在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。就這麼一點點繼續往下推理。spa
1.餘數法
code
他給的函數接口是這樣blog
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到對尾,如此往復,直到隊列爲空。leetcode
我當時的想法時隊列空了應該就能解出來了吧。。。因而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,所以損失的空間複雜度也能接受。
寫了這個着實激發了我很大的興趣,因而後面又寫了生成題庫的模塊,圖形界面的模塊。。。