最近在一個數獨網站玩數獨游戲,網站地址為:http://www.sudokufans.org.cn/。
由於自己數獨能力不是特別強,解題比較慢,但是自己是程序猿,所以,我想,自己寫個數獨計算器吧,讓電腦幫我去算得了。
由於我是C程序猿,所以第一步要做的是,先不管界面,做一個黑底白字的win32控制台應用程序,用於驗證自己的算法。
好了,開工,所以做一個簡單的兒童4階數獨,如圖:
,
我程序是這樣使用的,首先,在程序的同目錄下放一個input.txt文件用於輸入,其中,未知數用0表示,每個數之間一個空格,例如上圖的input.txt文件的內容為:
4 0 3 0
3 0 0 0
0 0 0 0
0 0 0 1
然后點擊根據我代碼生成的程序,就得到輸出結果。
。
由於數據比較簡單,比較才是4X4的數獨,所以也沒做什么優化,就是通過數據結構里圖論中的DFS從第一個未知數開始,至上而下,從左到右依次枚舉每種可能解。
代碼如下:
1 #include <iostream> 2 #include <fstream> 3 #include <set> 4 #include <vector> 5 using namespace std; 6 7 //#define DEBUG 8 9 vector<vector<int> > Sudo; 10 11 void PrintSudo() 12 { 13 for (int i=0; i<4; i++) 14 { 15 for (int j=0; j<4; j++) 16 { 17 cout << Sudo[i][j] << " "; 18 } 19 cout << endl; 20 } 21 } 22 23 bool DFS(int X, int Y) 24 { 25 if (Y >= 4) 26 { 27 return true; 28 } 29 30 if (X >= 4) 31 { 32 return DFS(0, Y + 1); 33 } 34 35 if (Sudo[Y][X] != 0) 36 { 37 return DFS(X + 1, Y); 38 } 39 40 set<int> HaveExist; 41 int i, j; 42 43 for (i=0; i<4; i++) 44 { 45 if (Sudo[Y][i] != 0) 46 { 47 HaveExist.insert(Sudo[Y][i]); //同行中已存在的數 48 } 49 50 if (Sudo[i][X] != 0) 51 { 52 HaveExist.insert(Sudo[i][X]); //同列中已存在的數 53 } 54 } 55 56 for (i=Y/2*2; i<Y/2*2 + 2; i++) 57 { 58 for (j=X/2*2; j<X/2*2 + 2; j++) 59 { 60 if (Sudo[i][j] != 0) 61 { 62 HaveExist.insert(Sudo[i][j]); 63 } 64 } 65 } 66 67 68 for (i=1; i<=4; i++) 69 { 70 if (HaveExist.find(i) == HaveExist.end()) //數字i在當前數獨還未存在,是候選數 71 { 72 Sudo[Y][X] = i; 73 #ifdef DEBUG 74 cout << "X=" << X << ", Y=" << Y << endl; 75 cout << "已存在的數:"; 76 77 for (set<int>::iterator it=HaveExist.begin(); it!=HaveExist.end(); it++) 78 { 79 cout << *it << " "; 80 } 81 cout << endl; 82 cout << "將Sudo[" << Y << "][" << X << "]設置成" << i << endl; 83 PrintSudo(); 84 #endif 85 if (DFS(X+1, Y)) 86 { 87 return true; 88 } 89 } 90 } 91 92 Sudo[Y][X] = 0; 93 return false; 94 } 95 96 int main() 97 { 98 ifstream cin("input.txt"); 99 100 101 102 for (int i=0; i<4; i++) 103 { 104 vector<int> vecTmp; 105 for (int j=0; j<4; j++) 106 { 107 int nTmp; 108 109 cin >> nTmp; 110 vecTmp.push_back(nTmp); 111 } 112 Sudo.push_back(vecTmp); 113 vecTmp.clear(); 114 } 115 116 if(!DFS(0, 0)) 117 { 118 cout << "輸入數據有誤" << endl; 119 } 120 121 for (int i=0; i<4; i++) 122 { 123 for (int j=0; j<4; j++) 124 { 125 cout << Sudo[i][j] << " "; 126 } 127 cout << endl; 128 } 129 130 while (true) 131 { 132 133 } 134 135 return 0; 136 }
好了,嘗試了4X4的數獨之后,再來嘗試9X9的數獨,首先,我們先簡單的將之前的4改成9(當然,同區域的56和58的2改成3)看看情況會怎么樣;
看代碼
1 #include <iostream> 2 #include <fstream> 3 #include <set> 4 #include <vector> 5 using namespace std; 6 7 //#define DEBUG 8 9 vector<vector<int> > Sudo; 10 11 void PrintSudo() 12 { 13 for (int i=0; i<9; i++) 14 { 15 for (int j=0; j<9; j++) 16 { 17 cout << Sudo[i][j] << " "; 18 } 19 cout << endl; 20 } 21 } 22 23 bool DFS(int X, int Y) 24 { 25 if (Y >= 9) 26 { 27 return true; 28 } 29 30 if (X >= 9) 31 { 32 return DFS(0, Y + 1); 33 } 34 35 if (Sudo[Y][X] != 0) 36 { 37 return DFS(X + 1, Y); 38 } 39 40 set<int> HaveExist; 41 int i, j; 42 43 for (i=0; i<9; i++) 44 { 45 if (Sudo[Y][i] != 0) 46 { 47 HaveExist.insert(Sudo[Y][i]); //同行中已存在的數 48 } 49 50 if (Sudo[i][X] != 0) 51 { 52 HaveExist.insert(Sudo[i][X]); //同列中已存在的數 53 } 54 } 55 56 for (i=Y/3*3; i<Y/3*3 + 3; i++) 57 { 58 for (j=X/3*3; j<X/3*3 + 3; j++) 59 { 60 if (Sudo[i][j] != 0) 61 { 62 HaveExist.insert(Sudo[i][j]); 63 } 64 } 65 } 66 67 68 for (i=1; i<=9; i++) 69 { 70 if (HaveExist.find(i) == HaveExist.end()) //數字i在當前數獨還未存在,是候選數 71 { 72 Sudo[Y][X] = i; 73 #ifdef DEBUG 74 cout << "X=" << X << ", Y=" << Y << endl; 75 cout << "已存在的數:"; 76 77 for (set<int>::iterator it=HaveExist.begin(); it!=HaveExist.end(); it++) 78 { 79 cout << *it << " "; 80 } 81 cout << endl; 82 cout << "將Sudo[" << Y << "][" << X << "]設置成" << i << endl; 83 PrintSudo(); 84 #endif 85 if (DFS(X+1, Y)) 86 { 87 return true; 88 } 89 } 90 } 91 92 Sudo[Y][X] = 0; 93 return false; 94 } 95 96 int main() 97 { 98 ifstream cin("input.txt"); 99 100 101 102 for (int i=0; i<9; i++) 103 { 104 vector<int> vecTmp; 105 for (int j=0; j<9; j++) 106 { 107 int nTmp; 108 109 cin >> nTmp; 110 vecTmp.push_back(nTmp); 111 } 112 Sudo.push_back(vecTmp); 113 vecTmp.clear(); 114 } 115 116 if(!DFS(0, 0)) 117 { 118 cout << "輸入數據有誤" << endl; 119 } 120 121 for (int i=0; i<9; i++) 122 { 123 for (int j=0; j<9; j++) 124 { 125 cout << Sudo[i][j] << " "; 126 } 127 cout << endl; 128 } 129 130 while (true) 131 { 132 133 } 134 135 return 0; 136 }
好,用上面提到的,在input.txt中輸入下面的數獨,測試一下。
我的能夠成功,速度也還接受得了。
好了,下面來點有難度的了。
例如下面的數獨:
這個數獨,用上面的程序計算的話,那就不是一般的慢了。
所以,必須考慮優化算法。
那么該怎么優化呢?我想先聽聽大家的看法。
本文待續......
首先分析下為什么上面的程序解上圖中的數獨時會很慢,因為前面的程序是暴力枚舉所以可能的情況,直到找到可行解為止,而這個數獨的已知數只有17個,而未知數卻有81-17=64個,我假設平均每個格子有4個可能解,那么人品不好的話,可能要嘗試4的64次方,這個數大得太恐怖了,所以必須進行剪枝。
怎么剪枝呢?我利用的是人腦解數獨的一些方法,為了方便描述,我將橫排編號為A-I,豎排編號為1-9,這樣左上角的坐標便是A1,右下角的坐標便是I9,我先假設每個格子都可以填1-9這九種可能的數字,然后根據已知數,不斷刪除每個格子的可能性數,例如上圖中根據已知數,可能得到可能性表:
。
接下了,就是很重要的優化步驟了,根據我們解數獨方法,我們可以知道,在右上區域,只有I1有可能值為5,該區域其他格子都沒有成為5的可能,所以I1必為5(玩過數獨的應該很容易理解)。確定I1為5后,又可以刪除同行、同列其他格子5的可能情況:
。
同理,可以確定E5=6,C9=6,等等,因此程序的流程已經比較清晰,由於不會畫流程圖,所以只能先用文字描述程序流程,求會畫流程圖的大神提供幫助。
1.初始化數獨的可能性表,讓每個格子都有1-9這9種可能;
2.輸入已知數,每輸入一個已知數,便確定了一個值;
3.根據該確定值刪除同行、同列、同區域中其他格子的該確定值的可能值;
4.在刪除格子可能值的時,判斷刪除完后,該格子是否只剩唯一的可能值了,如果是,則說明又確定一個格子的值,執行步驟3;
5.輸入完已知數后,判斷每個格子包含的可能值是該行或該列或該區域其他格子的可能性表中沒有的,則可確定該格的值便是這個特有的可能值,執行步驟3.
6.對於剩下的未知數,根據其可能性表做DFS,求得最終可行解。
代碼如下:
1 #include <iostream> 2 #include <fstream> 3 #include <list> 4 #include <vector> 5 #include <algorithm> 6 using namespace std; 7 8 const int SUDOSIZE = 9; 9 10 //#define DEBUG 11 typedef vector<vector<list<int> > > SudoPoss_t; 12 SudoPoss_t g_SudoPoss; //數獨每個位置可選擇數字集合數組 13 14 inline int IntSqrt(int n); 15 //初始化可能性表 16 //一開始每個格子都有填1-9的可能 17 void Init(); 18 void ShowGridPossiNums(SudoPoss_t SudoPoss, const int X, const int Y); 19 bool AssumeOneValue(SudoPoss_t &SudoPoss, const int& X, const int& Y, const int& Value); 20 bool IsOnlyPossibleInSameRow(int X, int Y, int PossiVal); 21 bool IsOnlyPossibleInSameCol(int X, int Y, int PossiVal); 22 bool IsOnlyPossibleInSameArea(int X, int Y, int PossiVal); 23 //例如 24 //0 2 0 0 1 0 0 0 0 25 //0 0 0 0 0 4 0 8 3 26 //0 0 0 0 0 5 0 7 X 27 //0 0 0 0 0 8 0 0 0 28 //7 0 0 0 0 3 0 0 0 29 //0 9 0 0 0 0 1 0 0 30 //8 0 0 0 0 0 0 0 0 31 //0 0 0 0 2 0 6 0 0 32 //0 0 0 0 9 0 0 4 0 33 //只有X可以為1,因為該區域內只有X有為1的可能性,所以可以確定X為1,排除此處其他可能性 34 void ConfirmOnlyPossible(); 35 void ReadInput(); 36 void ShowAllPossNums(); 37 bool DFS(SudoPoss_t SudoPoss, int X, int Y); 38 39 int main() 40 { 41 42 43 Init(); 44 45 try 46 { 47 ReadInput(); 48 } 49 catch (int e) 50 { 51 cout << "輸入數獨數據錯誤" << endl; 52 return -1; 53 } 54 55 ConfirmOnlyPossible(); 56 57 if(!DFS(g_SudoPoss, 0, 0)) 58 { 59 cout << "輸入數據有誤" << endl; 60 } 61 62 for (int i=0; i<SUDOSIZE; i++) 63 { 64 for (int j=0; j<SUDOSIZE; j++) 65 { 66 cout << *g_SudoPoss[i][j].begin() << " "; 67 } 68 cout << endl; 69 } 70 71 while(true) 72 { 73 74 } 75 76 return 0; 77 } 78 79 inline int IntSqrt(int n) 80 { 81 if (1 == n || 0 == n) 82 { 83 return n; 84 } 85 86 for (int i = n / 2; i>=1; i--) 87 { 88 if (i * i == n) 89 { 90 return i; 91 } 92 } 93 return -1; 94 } 95 96 97 //初始化可能性表 98 //一開始每個格子都有填1-9的可能 99 void Init() 100 { 101 //set<int> setTmp; 102 list<int> listTmp; 103 104 for (int i=1; i<=9; i++) 105 { 106 listTmp.push_back(i); 107 } 108 109 vector<list<int> > vecTmp; 110 for (int j=0; j<SUDOSIZE; j++) 111 { 112 vecTmp.push_back(listTmp); 113 } 114 115 for (int i=0; i<SUDOSIZE; i++) 116 { 117 g_SudoPoss.push_back(vecTmp); 118 } 119 } 120 121 //顯示第(X,Y)位置格子可供選擇的數字 122 void ShowGridPossiNums(SudoPoss_t SudoPoss, const int X, const int Y) 123 { 124 cout << "SudoPoss[" << Y << "][" << X << "] :" ; 125 for (list<int>::iterator it=SudoPoss[Y][X].begin(); it!=SudoPoss[Y][X].end(); it++) 126 { 127 cout << " " << *it ; 128 } 129 cout << " size() = " << SudoPoss[Y][X].size(); 130 cout << endl; 131 } 132 133 //假設(X,Y)位置處確定為值Value,刪除其他位置的Value值的可能情況,從而進行剪枝 134 bool AssumeOneValue(SudoPoss_t &SudoPoss, const int& X, const int& Y, const int& Value) 135 { 136 if (SudoPoss[Y][X].size() == 0) 137 { 138 return false; 139 } 140 141 //如果某個位置是已知數,則將該位置其他可能數刪除 142 for (list<int>::iterator it=SudoPoss[Y][X].begin(); it!=SudoPoss[Y][X].end(); ) 143 { 144 if (*it != Value) 145 { 146 SudoPoss[Y][X].erase(it); 147 it=SudoPoss[Y][X].begin(); 148 continue; 149 } 150 151 it++; 152 } 153 154 //在同行中其他格子中刪除該已知數 155 for (int i=0; i<SUDOSIZE; i++) 156 { 157 if (i == X) 158 { 159 continue; 160 } 161 162 list<int>::iterator it = find(SudoPoss[Y][i].begin(), SudoPoss[Y][i].end(), Value); 163 if (it != SudoPoss[Y][i].end()) 164 { 165 SudoPoss[Y][i].erase(it); 166 167 //如果某格沒有任何可能的情況 則表示該推測錯誤 168 if (0 == SudoPoss[Y][i].size()) 169 { 170 return false; 171 } 172 //通過剪枝使某一個只有一種可能的情況,則針對該格繼續剪枝 173 else if (1 == SudoPoss[Y][i].size()) 174 { 175 if (!AssumeOneValue(SudoPoss, i, Y, *SudoPoss[Y][i].begin())) 176 { 177 return false; 178 } 179 } 180 } 181 } 182 183 //在同列中其他格子刪除該已知數 184 for (int i=0; i<SUDOSIZE; i++) 185 { 186 if (i == Y) 187 { 188 continue; 189 } 190 191 list<int>::iterator it = find(SudoPoss[i][X].begin(), SudoPoss[i][X].end(), Value); 192 if (it != SudoPoss[i][X].end()) 193 { 194 SudoPoss[i][X].erase(it); 195 196 if (0 == SudoPoss[i][X].size()) 197 { 198 return false; 199 } 200 else if (1 == SudoPoss[i][X].size()) 201 { 202 if (!AssumeOneValue(SudoPoss, X, i, *SudoPoss[i][X].begin())) 203 { 204 return false; 205 } 206 } 207 } 208 } 209 210 //在同區域中其他格子刪除該已知數 211 for (int i=Y/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE); i<Y/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE) + IntSqrt(SUDOSIZE); i++) 212 { 213 for (int j=X/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE); j<X/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE) + IntSqrt(SUDOSIZE); j++) 214 { 215 if (i == Y && j == X) 216 { 217 continue; 218 } 219 220 list<int>::iterator it = find(SudoPoss[i][j].begin(), SudoPoss[i][j].end(), Value); 221 if (it != SudoPoss[i][j].end()) 222 { 223 SudoPoss[i][j].erase(it); 224 225 if (0 == SudoPoss[i][j].size()) 226 { 227 return false; 228 } 229 else if (1 == SudoPoss[i][j].size()) 230 { 231 if (!AssumeOneValue(SudoPoss, j, i, *SudoPoss[i][j].begin())) 232 { 233 return false; 234 } 235 } 236 } 237 } 238 } 239 return true; 240 } 241 242 bool IsOnlyPossibleInSameRow(int X, int Y, int PossiVal) 243 { 244 for (int i=0; i<SUDOSIZE; i++) 245 { 246 if (i == X) 247 { 248 continue; 249 } 250 251 if(find(g_SudoPoss[Y][i].begin(), g_SudoPoss[Y][i].end(), PossiVal) != g_SudoPoss[Y][i].end()) 252 { 253 return false; 254 } 255 } 256 return true; 257 } 258 259 bool IsOnlyPossibleInSameCol(int X, int Y, int PossiVal) 260 { 261 for (int i=0; i<SUDOSIZE; i++) 262 { 263 if (i == Y) 264 { 265 continue; 266 } 267 268 if(find(g_SudoPoss[i][X].begin(), g_SudoPoss[i][X].end(), PossiVal) != g_SudoPoss[i][X].end()) 269 { 270 return false; 271 } 272 } 273 return true; 274 } 275 276 bool IsOnlyPossibleInSameArea(int X, int Y, int PossiVal) 277 { 278 //在同區域中其他格子刪除該已知數 279 for (int i=Y/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE); i<Y/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE) + IntSqrt(SUDOSIZE); i++) 280 { 281 for (int j=X/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE); j<X/IntSqrt(SUDOSIZE)*IntSqrt(SUDOSIZE) + IntSqrt(SUDOSIZE); j++) 282 { 283 if (i == Y && j == X) 284 { 285 continue; 286 } 287 288 if(find(g_SudoPoss[i][j].begin(), g_SudoPoss[i][j].end(), PossiVal) != g_SudoPoss[i][j].end()) 289 { 290 return false; 291 } 292 } 293 } 294 return true; 295 } 296 297 //例如 298 //0 2 0 0 1 0 0 0 0 299 //0 0 0 0 0 4 0 8 3 300 //0 0 0 0 0 5 0 7 X 301 //0 0 0 0 0 8 0 0 0 302 //7 0 0 0 0 3 0 0 0 303 //0 9 0 0 0 0 1 0 0 304 //8 0 0 0 0 0 0 0 0 305 //0 0 0 0 2 0 6 0 0 306 //0 0 0 0 9 0 0 4 0 307 //只有X可以為1,因為該區域內只有X有為1的可能性,所以可以確定X為1,排除此處其他可能性 308 void ConfirmOnlyPossible() 309 { 310 for (int i=0; i<SUDOSIZE; i++) 311 { 312 for (int j=0; j<SUDOSIZE; j++) 313 { 314 if (g_SudoPoss[i][j].size() == 1) 315 { 316 continue; 317 } 318 319 for (list<int>::iterator it=g_SudoPoss[i][j].begin(); it!=g_SudoPoss[i][j].end(); it++) 320 { 321 if (IsOnlyPossibleInSameArea(j, i, *it) 322 || IsOnlyPossibleInSameCol(j, i, *it) 323 || IsOnlyPossibleInSameRow(j, i, *it)) 324 { 325 // cout << "確定Sudo[" << i << "][" << j << "]為" << *it << endl; 326 AssumeOneValue(g_SudoPoss, j, i, *it); 327 //重新開始循環 328 i = -1; 329 j = SUDOSIZE; 330 break; 331 } 332 } 333 } 334 } 335 } 336 337 void ReadInput() 338 { 339 ifstream cin("input.txt"); 340 341 for (int i=0; i<SUDOSIZE; i++) 342 { 343 for (int j=0; j<SUDOSIZE; j++) 344 { 345 int nTmp; 346 347 cin >> nTmp; 348 349 if (0 == nTmp) 350 { 351 continue; 352 } 353 354 if (!AssumeOneValue(g_SudoPoss, j, i, nTmp)) 355 { 356 throw 0; 357 } 358 } 359 } 360 cin.close(); 361 } 362 void ShowAllPossNums() 363 { 364 for (int i=0; i<SUDOSIZE; i++) 365 { 366 for (int j=0; j<SUDOSIZE; j++) 367 { 368 ShowGridPossiNums(g_SudoPoss, j, i); 369 } 370 } 371 } 372 bool DFS(SudoPoss_t SudoPoss, int X, int Y) 373 { 374 if (Y >= SUDOSIZE) 375 { 376 g_SudoPoss = SudoPoss; 377 return true; 378 } 379 380 if (X >= SUDOSIZE) 381 { 382 return DFS(SudoPoss, 0, Y + 1); 383 } 384 385 if (SudoPoss[Y][X].size() == 1) 386 { 387 return DFS(SudoPoss, X + 1, Y); 388 } 389 390 for (list<int>::iterator it=SudoPoss[Y][X].begin(); it!=SudoPoss[Y][X].end(); it++) 391 { 392 SudoPoss_t TmpSudoPoss = SudoPoss; 393 394 if (!AssumeOneValue(TmpSudoPoss,X, Y, *it)) 395 { 396 continue; 397 } 398 if (!DFS(TmpSudoPoss, X + 1, Y)) 399 { 400 continue; 401 } 402 else 403 { 404 return true; 405 } 406 } 407 return false; 408 }
底層算法暫時到這里,歡迎大家繼續提出優化建議,至於前端的界面,我目前也還正在學習win32GUI編程,等掌握好后,再做前端界面。
所以,本文待續......