1. 程式人生 > >C# 數獨求解演算法。

C# 數獨求解演算法。

前言

       數獨是一種有趣的智力遊戲,但是部分高難度數獨在求解過程中經常出現大量單元格有多個候選數字可以填入,不得不嘗試填寫某個數字然後繼續推導的方法。不幸的是這種方法經常出現填到一半才發現有單元格無數可填,說明之前就有單元格填錯了把後面的路堵死了。這時就需要悔步,之前的單元格換個數重新試。然而更坑的是究竟要悔多少步呢?不知道。要換數字的時候該換哪個呢?也不知道。手算時就需要大量草稿紙記錄填寫情況,不然容易忘了哪些試過哪些沒試過。

       在朋友那裡玩他手機上的數獨的時候就發現這個問題很煩,到這裡其實就不是一個智力遊戲,而是體力遊戲了。這種體力活實際上交給電腦才是王道。網上搜了一圈,大多都是Java、vb、C++之類的實現,且多是遞迴演算法。遞迴有一個問題,隨著問題規模的擴大,很容易不小心就把棧撐爆,而且大多數實現只是求出答案就完了,很多求解中的資訊就沒了,而我更想看看這些過程資訊。改別人的程式碼實在是太蛋疼,想了想,不如自己重新寫一個。

 

正文

       說回正題,先簡單說明一下演算法思路(標準數獨):

       1、先尋找並填寫那些唯一數單元格。在部分數獨中有些單元格會因為同行、列、宮內題目已知數的限制,實際只有一個數可以填,這種單元格就應該趁早填好,因為沒有嘗試的必要,不提前處理掉還會影響之後求解的效率。在填寫數字後,同行、列、宮的候選數就會減少,可能會出現新的唯一數單元格,那麼繼續填寫,直到沒有唯一數單元格為止。

       2、檢查是否已經完成遊戲,也就是所有單元格都有數字。部分簡單數獨一直填唯一數單元格就可以完成遊戲。

       3、按照從單元格左到右、從上到下,數字從小到大的順序嘗試填寫有多個候選數的單元格,直到全部填完或者發現有單元格候選數為空。如果出現無候選數的單元格說明之前填錯數導致出現死路,就需要悔步清除上一個單元格填過的數,換成下一個候選數繼續嘗試。如果清除後發現沒有更大的候選數可填,說明更早之前就已經填錯了,要繼續悔步並換下一個候選數。有可能需要連續悔多步,一直悔步直到有更大的候選數可填的單元格。如果一路到最開始的單元格都沒法填,說明這個數獨有問題,無解。

 

       程式碼(包括數獨求解器,求解過程資訊,答案儲存三個主要類):

       數獨求解器

  1     public class SudokuSolver
  2     {
  3         /// <summary>
  4         /// 題目面板
  5         /// </summary>
  6         public SudokuBlock[][] SudokuBoard { get; }
  7 
  8         public SudokuSolver(byte[][] board)
  9         {
 10             SudokuBoard = new SudokuBlock[board.Length][];
 11             //初始化數獨的行
 12             for (int i = 0; i < board.Length; i++)
 13             {
 14                 SudokuBoard[i] = new SudokuBlock[board[i].Length];
 15                 //初始化每行的列
 16                 for (int j = 0; j < board[i].Length; j++)
 17                 {
 18                     SudokuBoard[i][j] = new SudokuBlock(
 19                         board[i][j] > 0
 20                         , board[i][j] <= 0 ? new BitArray(board.Length) : null
 21                         , board[i][j] > 0 ? (byte?)board[i][j] : null
 22                         , (byte)i
 23                         , (byte)j);
 24                 }
 25             }
 26         }
 27 
 28         /// <summary>
 29         /// 求解數獨
 30         /// </summary>
 31         /// <returns>獲得的解</returns>
 32         public IEnumerable<(SudokuState sudoku, PathTree path)> Solve(bool multiAnswer = false)
 33         {
 34             //初始化各個單元格能填入的數字
 35             InitCandidate();
 36 
 37             var pathRoot0 = new PathTree(null, -1, -1, -1); //填寫路徑樹,在非遞迴方法中用於記錄回退路徑和其他有用資訊,初始生成一個根
 38             var path0 = pathRoot0;
 39 
 40             //迴圈填入能填入的數字只有一個的單元格,每次填入都可能產生新的唯一數單元格,直到沒有唯一數單元格可填
 41             while (true)
 42             {
 43                 if (!FillUniqueNumber(ref path0))
 44                 {
 45                     break;
 46                 }
 47             }
 48 
 49             //檢查是否在填唯一數單元格時就已經把所有單元格填滿了
 50             var finish = true;
 51             foreach (var row in SudokuBoard)
 52             {
 53                 foreach (var cell in row)
 54                 {
 55                     if (!cell.IsCondition && !cell.IsUnique)
 56                     {
 57                         finish = false;
 58                         break;
 59                     }
 60                 }
 61                 if (!finish)
 62                 {
 63                     break;
 64                 }
 65             }
 66             if (finish)
 67             {
 68                 yield return (new SudokuState(this), path0);
 69                 yield break;
 70             }
 71 
 72             var pathRoot = new PathTree(null, -1, -1, -1); //填寫路徑樹,在非遞迴方法中用於記錄回退路徑和其他有用資訊,初始生成一個根
 73             var path = pathRoot;
 74             var toRe = new List<(SudokuState sudoku, PathTree path)>();
 75             //還存在需要試數才能求解的單元格,開始暴力搜尋
 76             int i = 0, j = 0;
 77             while (true)
 78             {
 79                 (i, j) = NextBlock(i, j);
 80 
 81                 //正常情況下返回-1表示已經全部填完
 82                 if (i == -1 && j == -1 && !multiAnswer)
 83                 {
 84                     var pathLast = path;//記住最後一步
 85                     var path1 = path;
 86                     while(path1.Parent.X != -1 && path1.Parent.Y != -1)
 87                     {
 88                         path1 = path1.Parent;
 89                     }
 90 
 91                     //將暴力搜尋的第一步追加到唯一數單元格的填寫步驟的最後一步之後,連線成完整的填數步驟
 92                     path0.Children.Add(path1);
 93                     path1.Parent = path0;
 94                     yield return (new SudokuState(this), pathLast);
 95                     break;
 96                 }
 97 
 98                 var numNode = path.Children.LastOrDefault();
 99                 //確定要從哪個數開始進行填入嘗試
100                 var num = numNode == null
101                     ? 0
102                     : numNode.Number;
103 
104                 bool filled = false; //是否發現可以填入的數
105                 //迴圈檢視從num開始接下來的候選數是否能填(num是最後一次填入的數,傳到Candidate[]的索引器中剛好指向 num + 1是否能填的儲存位,對於標準數獨,候選數為 1~9,Candidate的索引範圍就是 0~8)
106                 for (; !SudokuBoard[i][j].IsCondition && !SudokuBoard[i][j].IsUnique && num < SudokuBoard[i][j].Candidate.Length; num++)
107                 {
108                     //如果有可以填的候選數,理論上不會遇見沒有可以填的情況,這種死路情況已經在UpdateCandidate時檢查了
109                     if (SudokuBoard[i][j].Candidate[num] && !path.Children.Any(x => x.Number - 1 == num && !x.Pass))
110                     {
111                         filled = true; //進來了說明單元格有數可以填
112                         //記錄步驟
113                         var node = new PathTree(SudokuBoard[i][j], i, j, num + 1, path);
114                         path = node;
115                         //如果更新相關單元格的候選數時發現死路(更新函式會在發現死路時自動撤銷更新)
116                         (bool canFill, (byte x, byte y)[] setList) updateResult = UpdateCandidate(i, j, (byte)(num + 1));
117                         if (!updateResult.canFill)
118                         {
119                             //記錄這條路是死路
120                             path.SetPass(false);
121                         }
122                         //僅在確認是活路時設定填入數字
123                         if (path.Pass)
124                         {
125                             SudokuBoard[i][j].SetNumber((byte)(num + 1));
126                             path.SetList = updateResult.setList;//記錄相關單元格可填數更新記錄,方便在回退時撤銷更新
127                         }
128                         else //出現死路,要進行回退,重試這個單元格的其他可填數字
129                         {
130                             path.Block.SetNumber(null);
131                             path = path.Parent;
132                         }
133                         //填入一個候選數後跳出迴圈,不再繼續嘗試填入之後的候選數
134                         break;
135                     }
136                 }
137                 if (!filled)//如果沒有成功填入數字,說明上一步填入的單元格就是錯的,會導致後面的單元格怎麼填都不對,要回退到上一個單元格重新填
138                 {
139                     path.SetPass(false);
140                     path.Block.SetNumber(null);
141                     foreach (var pos in path.SetList)
142                     {
143                         SudokuBoard[pos.x][pos.y].Candidate.Set(path.Number - 1, true);
144                     }
145                     path = path.Parent;
146                     i = path.X < 0 ? 0 : path.X;
147                     j = path.Y < 0 ? 0 : path.Y;
148                 }
149             }
150         }
151 
152         /// <summary>
153         /// 初始化候選項
154         /// </summary>
155         private void InitCandidate()
156         {
157             //初始化每行空缺待填的數字
158             var rb = new List<BitArray>();
159             for (int i = 0; i < SudokuBoard.Length; i++)
160             {
161                 var r = new BitArray(SudokuBoard.Length);
162                 r.SetAll(true);
163                 for (int j = 0; j < SudokuBoard[i].Length; j++)
164                 {
165                     //如果i行j列是條件(題目)給出的數,設定數字不能再填(r[x] == false 表示 i 行不能再填 x + 1,下標加1表示數獨可用的數字,下標對應的值表示下標加1所表示的數是否還能填入該行)
166                     if (SudokuBoard[i][j].IsCondition || SudokuBoard[i][j].IsUnique)
167                     {
168                         r.Set(SudokuBoard[i][j].Number.Value - 1, false);
169                     }
170                 }
171                 rb.Add(r);
172             }
173 
174             //初始化每列空缺待填的數字
175             var cb = new List<BitArray>();
176             for (int j = 0; j < SudokuBoard[0].Length; j++)
177             {
178                 var c = new BitArray(SudokuBoard[0].Length);
179                 c.SetAll(true);
180                 for (int i = 0; i < SudokuBoard.Length; i++)
181                 {
182                     if (SudokuBoard[i][j].IsCondition || SudokuBoard[i][j].IsUnique)
183                     {
184                         c.Set(SudokuBoard[i][j].Number.Value - 1, false);
185                     }
186                 }
187                 cb.Add(c);
188             }
189 
190             //初始化每宮空缺待填的數字(目前只能算標準 n×n 數獨的宮)
191             var gb = new List<BitArray>();
192             //n表示每宮應有的行、列數(標準數獨行列、數相同)
193             var n = (int)Sqrt(SudokuBoard.Length);
194             for (int g = 0; g < SudokuBoard.Length; g++)
195             {
196                 var gba = new BitArray(SudokuBoard.Length);
197                 gba.SetAll(true);
198                 for (int i = g / n * n; i < g / n * n + n; i++)
199                 {
200                     for (int j = g % n * n; j < g % n * n + n; j++)
201                     {
202                         if (SudokuBoard[i][j].IsCondition || SudokuBoard[i][j].IsUnique)
203                         {
204                             gba.Set(SudokuBoard[i][j].Number.Value - 1, false);
205                         }
206                     }
207                 }
208                 gb.Add(gba);
209             }
210 
211             //初始化每格可填的候選數字
212             for (int i = 0; i < SudokuBoard.Length; i++)
213             {
214                 for (int j = 0; j < SudokuBoard[i].Length; j++)
215                 {
216 
217                     if (!SudokuBoard[i][j].IsCondition)
218                     {
219                         var c = SudokuBoard[i][j].Candidate;
220                         c.SetAll(true);
221                         //當前格能填的數為其所在行、列、宮同時空缺待填的數字,按位與運算後只有同時能填的候選數保持1(可填如當前格),否則變成0
222                         // i / n * n + j / n:根據行號列號計算宮號,
223                         c = c.And(rb[i]).And(cb[j]).And(gb[i / n * n + j / n]);
224                         SudokuBoard[i][j].SetCandidate(c);
225                     }
226                 }
227             }
228         }
229 
230         /// <summary>
231         /// 求解開始時尋找並填入單元格唯一可填的數,減少解空間
232         /// </summary>
233         /// <returns>是否填入過數字,如果為false,表示能立即確定待填數字的單元格已經沒有,要開始暴力搜尋了</returns>
234         private bool FillUniqueNumber(ref PathTree path)
235         {
236             var filled = false;
237             for (int i = 0; i < SudokuBoard.Length; i++)
238             {
239                 for (int j = 0; j < SudokuBoard[i].Length; j++)
240                 {
241                     if (!SudokuBoard[i][j].IsCondition && !SudokuBoard[i][j].IsUnique)
242                     {
243                         var canFillCount = 0;
244                         var index = -1;
245                         for (int k = 0; k < SudokuBoard[i][j].Candidate.Length; k++)
246                         {
247                             if (SudokuBoard[i][j].Candidate[k])
248                             {
249                                 index = k;
250                                 canFillCount++;
251                             }
252                             if (canFillCount > 1)
253                             {
254                                 break;
255                             }
256                         }
257                         if (canFillCount == 0)
258                         {
259                             throw new Exception("有單元格無法填入任何數字,數獨無解");
260                         }
261                         if (canFillCount == 1)
262                         {
263                             var num = (byte)(index + 1);
264                             SudokuBoard[i][j].SetNumber(num);
265                             SudokuBoard[i][j].SetUnique();
266                             filled = true;
267                             var upRes = UpdateCandidate(i, j, num);
268                             if (!upRes.canFill)
269                             {
270                                 throw new Exception("有單元格無法填入任何數字,數獨無解");
271                             }
272                             path = new PathTree(SudokuBoard[i][j], i, j, num, path);
273                             path.SetList = upRes.setList;
274                         }
275                     }
276                 }
277             }
278             return filled;
279         }
280 
281         /// <summary>
282         /// 更新單元格所在行、列、宮的其它單元格能填的數字候選,如果沒有,會撤銷更新
283         /// </summary>
284         /// <param name="row">行號</param>
285         /// <param name="column">列號</param>
286         /// <param name="canNotFillNumber">要剔除的候選數字</param>
287         /// <returns>更新候選數後,所有被更新的單元格是否都有可填的候選數字</returns>
288         private (bool canFill, (byte x, byte y)[] setList) UpdateCandidate(int row, int column, byte canNotFillNumber)
289         {
290             var canFill = true;
291             var list = new List<SudokuBlock>(); // 記錄修改過的單元格,方便撤回修改
292 
293             bool CanFillNumber(int i, int j)
294             {
295                 var re = true;
296                 var _canFill = false;
297                 for (int k = 0; k < SudokuBoard[i][j].Candidate.Length; k++)
298                 {
299                     if (SudokuBoard[i][j].Candidate[k])
300                     {
301                         _canFill = true;
302                         break;
303                     }
304                 }
305                 if (!_canFill)
306                 {
307                     re = false;
308                 }
309 
310                 return re;
311             }
312             bool Update(int i, int j)
313             {
314                 if (!(i == row && j == column) && !SudokuBoard[i][j].IsCondition && !SudokuBoard[i][j].IsUnique && SudokuBoard[i][j].Candidate[canNotFillNumber - 1])
315                 {
316                     SudokuBoard[i][j].Candidate.Set(canNotFillNumber - 1, false);
317                     list.Add(SudokuBoard[i][j]);
318 
319                     return CanFillNumber(i, j);
320                 }
321                 else
322                 {
323                     return true;
324                 }
325             }
326 
327             //更新該行其餘列
328             for (int j = 0; j < SudokuBoard[row].Length; j++)
329             {
330                 canFill = Update(row, j);
331                 if (!canFill)
332                 {
333                     break;
334                 }
335             }
336 
337             if (canFill) //只在行更新時沒發現無數可填的單元格時進行列更新才有意義
338             {
339                 //更新該列其餘行
340                 for (int i = 0; i < SudokuBoard.Length; i++)
341                 {
342                     canFill = Update(i, column);
343                     if (!canFill)
344                     {
345                         break;
346                     }
347                 }
348             }
349 
350             if (canFill)//只在行、列更新時都沒發現無數可填的單元格時進行宮更新才有意義
351             {
352                 //更新該宮其餘格
353                 //n表示每宮應有的行、列數(標準數獨行列、數相同)
354                 var n = (int)Sqrt(SudokuBoard.Length);
355                 //g為宮的編號,根據行號列號計算
356                 var g = row / n * n + column / n;
357                 for (int i = g / n * n; i < g / n * n + n; i++)
358                 {
359                     for (int j = g % n * n; j < g % n * n + n; j++)
360                     {
361                         canFill = Update(i, j);
362                         if (!canFill)
363                         {
364                             goto canNotFill;
365                         }
366                     }
367                 }
368                 canNotFill:;
369             }
370 
371             //如果發現存在沒有任何數字可填的單元格,撤回所有候選修改
372             if (!canFill)
373             {
374                 foreach (var cell in list)
375                 {
376                     cell.Candidate.Set(canNotFillNumber - 1, true);
377                 }
378             }
379 
380             return (canFill, list.Select(x => (x.X, x.Y)).ToArray());
381         }
382 
383         /// <summary>
384         /// 尋找下一個要嘗試填數的格
385         /// </summary>
386         /// <param name="i">起始行號</param>
387         /// <param name="j">起始列號</param>
388         /// <returns>找到的下一個行列號,沒有找到返回-1</returns>
389         private (int x, int y) NextBlock(int i = 0, int j = 0)
390         {
391             for (; i < SudokuBoard.Length; i++)
392             {
393                 for (; j < SudokuBoard[i].Length; j++)
394                 {
395                     if (!SudokuBoard[i][j].IsCondition && !SudokuBoard[i][j].IsUnique && !SudokuBoard[i][j].Number.HasValue)
396                     {
397                         return (i, j);
398                     }
399                 }
400                 j = 0;
401             }
402 
403             return (-1, -1);
404         }
405 
406         public override string ToString()
407         {
408             static string Str(SudokuBlock b)
409             {
410                 var n1 = new[] { "①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨" };
411                 var n2 = new[] { "⑴", "⑵", "⑶", "⑷", "⑸", "⑹", "⑺", "⑻", "⑼" };
412                 return b.Number.HasValue
413                     ? b.IsCondition
414                         ? " " + b.Number
415                         : b.IsUnique
416                             ? n1[b.Number.Value - 1]
417                             : n2[b.Number.Value - 1]
418                     : "▢";
419             }
420             return
421 $@"{Str(SudokuBoard[0][0])},{Str(SudokuBoard[0][1])},{Str(SudokuBoard[0][2])},{Str(SudokuBoard[0][3])},{Str(SudokuBoard[0][4])},{Str(SudokuBoard[0][5])},{Str(SudokuBoard[0][6])},{Str(SudokuBoard[0][7])},{Str(SudokuBoard[0][8])}
422 {Str(SudokuBoard[1][0])},{Str(SudokuBoard[1][1])},{Str(SudokuBoard[1][2])},{Str(SudokuBoard[1][3])},{Str(SudokuBoard[1][4])},{Str(SudokuBoard[1][5])},{Str(SudokuBoard[1][6])},{Str(SudokuBoard[1][7])},{Str(SudokuBoard[1][8])}
423 {Str(SudokuBoard[2][0])},{Str(SudokuBoard[2][1])},{Str(SudokuBoard[2][2])},{Str(SudokuBoard[2][3])},{Str(SudokuBoard[2][4])},{Str(SudokuBoard[2][5])},{Str(SudokuBoard[2][6])},{Str(SudokuBoard[2][7])},{Str(SudokuBoard[2][8])}
424 {Str(SudokuBoard[3][0])},{Str(SudokuBoard[3][1])},{Str(SudokuBoard[3][2])},{Str(SudokuBoard[3][3])},{Str(SudokuBoard[3][4])},{Str(SudokuBoard[3][5])},{Str(SudokuBoard[3][6])},{Str(SudokuBoard[3][7])},{Str(SudokuBoard[3][8])}
425 {Str(SudokuBoard[4][0])},{Str(SudokuBoard[4][1])},{Str(SudokuBoard[4][2])},{Str(SudokuBoard[4][3])},{Str(SudokuBoard[4][4])},{Str(SudokuBoard[4][5])},{Str(SudokuBoard[4][6])},{Str(SudokuBoard[4][7])},{Str(SudokuBoard[4][8])}
426 {Str(SudokuBoard[5][0])},{Str(SudokuBoard[5][1])},{Str(SudokuBoard[5][2])},{Str(SudokuBoard[5][3])},{Str(SudokuBoard[5][4])},{Str(SudokuBoard[5][5])},{Str(SudokuBoard[5][6])},{Str(SudokuBoard[5][7])},{Str(SudokuBoard[5][8])}
427 {Str(SudokuBoard[6][0])},{Str(SudokuBoard[6][1])},{Str(SudokuBoard[6][2])},{Str(SudokuBoard[6][3])},{Str(SudokuBoard[6][4])},{Str(SudokuBoard[6][5])},{Str(SudokuBoard[6][6])},{Str(SudokuBoard[6][7])},{Str(SudokuBoard[6][8])}
428 {Str(SudokuBoard[7][0])},{Str(SudokuBoard[7][1])},{Str(SudokuBoard[7][2])},{Str(SudokuBoard[7][3])},{Str(SudokuBoard[7][4])},{Str(SudokuBoard[7][5])},{Str(SudokuBoard[7][6])},{Str(SudokuBoard[7][7])},{Str(SudokuBoard[7][8])}
429 {Str(SudokuBoard[8][0])},{Str(SudokuBoard[8][1])},{Str(SudokuBoard[8][2])},{Str(SudokuBoard[8][3])},{Str(SudokuBoard[8][4])},{Str(SudokuBoard[8][5])},{Str(SudokuBoard[8][6])},{Str(SudokuBoard[8][7])},{Str(SudokuBoard[8][8])}";
430         }
431     }
View Code

       大多數都有註釋,配合註釋應該不難理解,如有問題歡迎評論區交流。稍微說一下,過載ToString是為了方便除錯和檢視狀態,其中空心方框表示未填寫數字的單元格,數字表示題目給出數字的單元格,圈數字表示唯一數單元格填寫的數字,括號數字表示有多個候選數通過嘗試(暴力搜尋)確定的數字。注意類檔案最上面有一個 using static System.Math; 匯入靜態類,不然每次呼叫數學函式都要 Math. ,很煩。

 

       求解過程資訊

 1     public class PathTree
 2     {
 3         public PathTree Parent { get; set; }
 4         public List<PathTree> Children { get; } = new List<PathTree>();
 5 
 6         public SudokuBlock Block { get; }
 7         public int X { get; }
 8         public int Y { get; }
 9         public int Number { get; }
10         public bool Pass { get; private set; } = true;
11         public (byte x, byte y)[] SetList { get; set; }
12 
13         public PathTree(SudokuBlock block, int x, int y, int number)
14         {
15             Block = block;
16             X = x;
17             Y = y;
18             Number = number;
19 
20         }
21 
22         public PathTree(SudokuBlock block, int row, int column, int number, PathTree parent)
23             : this(block, row, column, number)
24         {
25             Parent = parent;
26             Parent.Children.Add(this);
27         }
28 
29         public void SetPass(bool pass)
30         {
31             Pass = pass;
32         }
33     }
View Code

       其中記錄了每個步驟在哪個單元格填寫了哪個數字,上一步是哪一步,之後嘗試過哪些步驟,這一步是否會導致之後的步驟出現死路,填寫數字後影響到的單元格和候選數字(用來在悔步的時候恢復相應單元格的候選數字)。

 

       答案儲存

 1     public class SudokuState
 2     {
 3         public SudokuBlock[][] SudokuBoard { get; }
 4         public SudokuState(SudokuSolver sudoku)
 5         {
 6             SudokuBoard = new SudokuBlock[sudoku.SudokuBoard.Length][];
 7             //初始化數獨的行
 8             for (int i = 0; i < sudoku.SudokuBoard.Length; i++)
 9             {
10                 SudokuBoard[i] = new SudokuBlock[sudoku.SudokuBoard[i].Length];
11                 //初始化每行的列
12                 for (int j = 0; j < sudoku.SudokuBoard[i].Length; j++)
13                 {
14                     SudokuBoard[i][j] = new SudokuBlock(
15                         sudoku.SudokuBoard[i][j].IsCondition
16                         , null
17                         , sudoku.SudokuBoard[i][j].Number
18                         , (byte)i
19                         , (byte)j);
20                     if (sudoku.SudokuBoard[i][j].IsUnique)
21                     {
22                         SudokuBoard[i][j].SetUnique();
23                     }
24                 }
25             }
26         }
27 
28         public override string ToString()
29         {
30             static string Str(SudokuBlock b)
31             {
32                 var n1 = new[] { "①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨" };
33                 var n2 = new[] { "⑴", "⑵", "⑶", "⑷", "⑸", "⑹", "⑺", "⑻", "⑼" };
34                 return b.Number.HasValue
35                     ? b.IsCondition
36                         ? " " + b.Number
37                         : b.IsUnique
38                             ? n1[b.Number.Value - 1]
39                             : n2[b.Number.Value - 1]
40                     : "▢";
41             }
42             return
43 $@"{Str(SudokuBoard[0][0])},{Str(SudokuBoard[0][1])},{Str(SudokuBoard[0][2])},{Str(SudokuBoard[0][3])},{Str(SudokuBoard[0][4])},{Str(SudokuBoard[0][5])},{Str(SudokuBoard[0][6])},{Str(SudokuBoard[0][7])},{Str(SudokuBoard[0][8])}
44 {Str(SudokuBoard[1][0])},{Str(SudokuBoard[1][1])},{Str(SudokuBoard[1][2])},{Str(SudokuBoard[1][3])},{Str(SudokuBoard[1][4])},{Str(SudokuBoard[1][5])},{Str(SudokuBoard[1][6])},{Str(SudokuBoard[1][7])},{Str(SudokuBoard[1][8])}
45 {Str(SudokuBoard[2][0])},{Str(SudokuBoard[2][1])},{Str(SudokuBoard[2][2])},{Str(SudokuBoard[2][3])},{Str(SudokuBoard[2][4])},{Str(SudokuBoard[2][5])},{Str(SudokuBoard[2][6])},{Str(SudokuBoard[2][7])},{Str(SudokuBoard[2][8])}
46 {Str(SudokuBoard[3][0])},{Str(SudokuBoard[3][1])},{Str(SudokuBoard[3][2])},{Str(SudokuBoard[3][3])},{Str(SudokuBoard[3][4])},{Str(SudokuBoard[3][5])},{Str(SudokuBoard[3][6])},{Str(SudokuBoard[3][7])},{Str(SudokuBoard[3][8])}
47 {Str(SudokuBoard[4][0])},{Str(SudokuBoard[4][1])},{Str(SudokuBoard[4][2])},{Str(SudokuBoard[4][3])},{Str(SudokuBoard[4][4])},{Str(SudokuBoard[4][5])},{Str(SudokuBoard[4][6])},{Str(SudokuBoard[4][7])},{Str(SudokuBoard[4][8])}
48 {Str(SudokuBoard[5][0])},{Str(SudokuBoard[5][1])},{Str(SudokuBoard[5][2])},{Str(SudokuBoard[5][3])},{Str(SudokuBoard[5][4])},{Str(SudokuBoard[5][5])},{Str(SudokuBoard[5][6])},{Str(SudokuBoard[5][7])},{Str(SudokuBoard[5][8])}
49 {Str(SudokuBoard[6][0])},{Str(SudokuBoard[6][1])},{Str(SudokuBoard[6][2])},{Str(SudokuBoard[6][3])},{Str(SudokuBoard[6][4])},{Str(SudokuBoard[6][5])},{Str(SudokuBoard[6][6])},{Str(SudokuBoard[6][7])},{Str(SudokuBoard[6][8])}
50 {Str(SudokuBoard[7][0])},{Str(SudokuBoard[7][1])},{Str(SudokuBoard[7][2])},{Str(SudokuBoard[7][3])},{Str(SudokuBoard[7][4])},{Str(SudokuBoard[7][5])},{Str(SudokuBoard[7][6])},{Str(SudokuBoard[7][7])},{Str(SudokuBoard[7][8])}
51 {Str(SudokuBoard[8][0])},{Str(SudokuBoard[8][1])},{Str(SudokuBoard[8][2])},{Str(SudokuBoard[8][3])},{Str(SudokuBoard[8][4])},{Str(SudokuBoard[8][5])},{Str(SudokuBoard[8][6])},{Str(SudokuBoard[8][7])},{Str(SudokuBoard[8][8])}";
52         }
53     }
View Code

       沒什麼好說的,就是儲存答案的,因為有些數獨的解不唯一,將來有機會擴充套件求多解時避免相互覆蓋。

 

       還有一個輔助類,單元格定義

 1     public class SudokuBlock
 2     {
 3         /// <summary>
 4         /// 填入的數字
 5         /// </summary>
 6         public byte? Number { get; private set; }
 7 
 8         /// <summary>
 9         /// X座標
10         /// </summary>
11         public byte X { get; }
12 
13         /// <summary>
14         /// Y座標
15         /// </summary>
16         public byte Y { get; }
17 
18         /// <summary>
19         /// 候選數字,下標所示狀態表示數字“下標加1”是否能填入
20         /// </summary>
21         public BitArray Candidate { get; private set; }
22 
23         /// <summary>
24         /// 是否為條件(題目)給出數字的單元格
25         /// </summary>
26         public bool IsCondition { get; }
27 
28         /// <summary>
29         /// 是否為遊戲開始就能確定唯一可填數字的單元格
30         /// </summary>
31         public bool IsUnique { get; private set; }
32 
33         public SudokuBlock(bool isCondition, BitArray candidate, byte? number, byte x, byte y)
34         {
35             IsCondition = isCondition;
36             Candidate = candidate;
37             Number = number;
38             IsUnique = false;
39             X = x;
40             Y = y;
41         }
42 
43         public void SetNumber(byte? number)
44         {
45             Number = number;
46         }
47 
48         public void SetCandidate(BitArray candidate)
49         {
50             Candidate = candidate;
51         }
52 
53         public void SetUnique()
54         {
55             IsUnique = true;
56         }
57     }
View Code

 

       測試程式碼

View Code

 

總結

       這個數獨求解器運用了大量 C# 7 的新特性,特別是 本地函式 和 基於 Tulpe 的簡寫的多返回值函式,能把本來一團亂的程式碼理清楚,寫清爽。 C# 果然是比 Java 這個躺在功勞簿上吃老本不求上進的坑爹語言爽多了。yield return 返回迭代器這種簡直是神仙設計,隨時想返回就返回,下次進來還能接著上次的地方繼續跑,寫這種程式碼簡直爽翻。另外目前多解求解功能還不可用,只是預留了集合返回型別和相關引數,以後看情況吧。

       如果你看過我的這篇文章 .Net Core 3 騷操作 之 用 Windows 桌面應用開發 Asp.Net Core 網站 ,你也可以在釋出啟動網站後訪問 https://localhost/Sudoku 來執行數獨求解器,注意,除錯狀態下埠為5001。

 

       轉載請完整保留以下內容,未經授權刪除以下內容進行轉載盜用的,保留追究法律責任的權利!

  本文地址:

  完整原始碼:Github

  裡面有各種小東西,這只是其中之一,不嫌棄的話可以Star一下。