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     }

大多数都有注释,配合注释应该不难理解,如有问题欢迎评论区交流。稍微说一下,重载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     }

其中记录了每个步骤在哪个单元格填写了哪个数字,上一步是哪一步,之后尝试过哪些步骤,这一步是否会导致之后的步骤出现死路,填写数字后影响到的单元格和候选数字(用来在悔步的时候恢复相应单元格的候选数字)。

答案存储

 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     }

没什么好说的,就是保存答案的,因为有些数独的解不唯一,将来有机会扩展求多解时避免相互覆盖。

还有一个辅助类,单元格定义

 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     }

测试代码

总结

这个数独求解器运用了大量 C# 7 的新特性,特别是 本地函数 和 基于 Tulpe 的简写的多返回值函数,能把本来一团乱的代码理清楚,写清爽。 C# 果然是比 Java 这个躺在功劳簿上吃老本不求上进的坑爹语言爽多了。yield return 返回迭代器这种简直是神仙设计,随时想返回就返回,下次进来还能接着上次的地方继续跑,写这种代码简直爽翻。另外目前多解求解功能还不可用,只是预留了集合返回类型和相关参数,以后看情况吧。

如果你看过我的这篇文章 .Net Core 3 骚操作 之 用 Windows 桌面应用开发 Asp.Net Core 网站 ,你也可以在发布启动网站后访问 https://localhost/Sudoku 来运行数独求解器,注意,调试状态下端口为5001。

转载请完整保留以下内容,未经授权删除以下内容进行转载盗用的,保留追究法律责任的权利!

  本文地址:

  完整源代码:Github

  里面有各种小东西,这只是其中之一,不嫌弃的话可以Star一下。

原文地址:https://www.cnblogs.com/coredx/p/12173702.html

时间: 2024-10-08 05:00:15

C# 数独求解算法。的相关文章

第四章 分治策略 4.1 最大子数组问题 (暴力求解算法)

/** * 最大子数组的暴力求解算法,复杂度为o(n2) * @param n * @return */ static MaxSubarray findMaxSubarraySlower(int[] n) { long tempSum = 0; int left = 0; int right = 0; long sum = Long.MIN_VALUE; for (int i = 0; i < n.length; i++) { for (int j = i; j < n.length; j++

致佳音: 推箱子游戏自动求解算法设计(四)

这一节是本文的核心内容,即推箱子游戏求解算法的设计思路过程 前面已经说过过,判断局面重复的最好标准不是局面完全一致,而是坐标排序相同且角色坐标通行 如下图,角色无论怎么移动,不推动箱子的时候,都能回到原来的位置,算作同一个局面: 再如下图,两个箱子互换位置,结果与没有移动箱子是一样的,所以排序箱子坐标以后一致,还是相同局面 问:有必要判断局面重复吗?是不是只是提升一下效率? 答:不是为了提升效率,而是为了能解出来,如果使用递归,重复的局面反复耗尽堆栈,而队列则耗尽内存 正如上图,反复推这两个箱子

[原创][网页游戏]数独生成算法及实例

[ 程序修正 2015/02/23 补充及订正方法:iphone上的Safari会自动对看起来像是电话号码的数字串(包括已经加入连字符或括号格式化过的)添加电话链接,点击之后会询问用户是否想要拨打该号码. 关闭方法: <meta name="format-detection" content="telephone=no" /> 单独开放方法: <a href="tel:13800138000">13800138000<

致佳音: 推箱子游戏自动求解算法设计(三)

这一节我们说说闭合曲线的填充,为什么会有这个东西呢 当我们递归一个场景时,我们以推动箱子为标志,如果不推动箱子,那么跑到哪里都白跑,而出现重复的判别最好就是所有坐标相同 包括这些坐标互换位置(排序结果相同),而后一个场景搬运工坐标能移动到另一个场景搬运工的位置(求解算法部分再详细说) 由于场景有多个箱子,每个箱子可以有几个方向移动,反复的寻路效率不高,起初我想删除路径部分,只检测能否移动到目标 来提升执行效率,就是偷懒一下,然后想想既然是礼物,偷懒也不是分时候,也有脸献给别人于是废弃了A×算法

致佳音: 推箱子游戏自己主动求解算法设计(四)

这一节是本文的核心内容,即推箱子游戏求解算法的设计思路过程 前面已经说过过,推断局面反复的最好标准不是局面全然一致,而是坐标排序同样且角色坐标通行 例如以下图.角色不管怎么移动,不推动箱子的时候.都能回到原来的位置.算作同一个局面: watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvcHJzbmlwZXI=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast"

编程之美之数独求解器的C++实现方法

编程之美的第一章的第15节,讲的是构造数独,一开始拿到这个问题的确没有思路, 不过看了书中的介绍之后, 发现原来这个的求解思路和N皇后问题是一致的, 但是不知道为啥,反正一开始确实没有想到这个回溯法,知道是用回溯法求解之后,问题就变得容易了很多. 这里我们不打算实现数独的构造,相反的,我们实现一个数独求解器,以后妈妈再也不用担心我的数独了. 当然求解器的思路和构造数独的思路一样,都是回溯法搜索,这里不再过多说明. 程序运行说明: 1.把待求解的数独数据放到in.txt文件中, 程序会自动读取他,

求解数独回溯算法

实现的java代码如下: //判断a[i][j]取值val是否有效 public boolean isValid(int[][] a, int i, int j, int val){ //判断是否跟同行冲突 for(int j1=0;j1<9;j1++){ if(a[i][j1]==val) return false; } //判断是否跟同列冲突 for(int i1=0;i1<9;i1++){ if(a[i1][j]==val) return false; } //找出a[i][j]所在的九

致佳音: 推箱子游戏自动求解算法设计(一)

本来酷爱音乐, 老衲也想谱一曲<献给爱丽丝>之类, 通俗又有境界的曲子, 奈何没有那个水平, 也不是一个程序员做的勾当,于是就有了本文. 希望莲花妹妹跟着思路走,能遗忘那些太多的忧伤-- 本文分以下四个小节: 一.平面寻路算法(Alpha Star) 二.闭合图形填充算法(扫描线种子填充) 三.推箱子求解 四.执行效率的优化 日本人有个程序叫Sokuban Automatic Solver,文件名是sokoban722.exe我附带在资源里面 不过日本人的东西没有开源,我们也不知道它里面的花花

回溯-数独求解

问题 数独(Sūdoku)是一种运用纸.笔进行演算的逻辑游戏.玩家需要根据9×9盘面上的已知数字,推理出所有剩余空格的数字,并满足每一行.每一列.每一个粗线宫内的数字均含1-9,不重复. 给定一个 9×9 二维数组 'grid[9][9]' 表示初始的局面,判断试放可求解,并输出最终的局面. 回溯法求解 对每一个空格子在放置一个数字前,我们先检查这个该数字对当前局面存在冲突,即检查当前行和当前列,和当前位置所走的 3×3的子格子.并递归的判断,当前放置的数字,是否可以求得一个解. 如果,不可以求