手把手教你寫數獨計算器(1)


    最近在一個數獨網站玩數獨游戲,網站地址為: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編程,等掌握好后,再做前端界面。

所以,本文待續......


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM