• 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

           测试代码

     1         static void Main(string[] args)
     2         {
     3             //模板
     4             //byte[][] game = new byte[][] {
     5             //    new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0},
     6             //    new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0},
     7             //    new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0},
     8             //    new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0},
     9             //    new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0},
    10             //    new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0},
    11             //    new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0},
    12             //    new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0},
    13             //    new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0},};
    14             //这个简单,无需尝试,一直填唯一数单元格,填完后剩下的单元格又有会变唯一数单元格
    15             //byte[][] game = new byte[][] {
    16             //    new byte[]{0, 5, 0, 7, 0, 6, 0, 1, 0},
    17             //    new byte[]{0, 8, 0, 0, 9, 0, 0, 6, 0},
    18             //    new byte[]{0, 6, 9, 0, 8, 0, 7, 3, 0},
    19             //    new byte[]{0, 1, 0, 0, 4, 0, 0, 0, 6},
    20             //    new byte[]{6, 0, 7, 1, 0, 3, 8, 0, 5},
    21             //    new byte[]{9, 0, 0, 0, 0, 8, 0, 2, 0},
    22             //    new byte[]{0, 2, 4, 0, 1, 0, 6, 5, 0},
    23             //    new byte[]{0, 7, 0, 0, 6, 0, 0, 4, 0},
    24             //    new byte[]{0, 9, 0, 4, 0, 2, 0, 8, 0},};
    25             //可以填一部分唯一数单元格,剩下一部分需要尝试,调试用
    26             //byte[][] game = new byte[][] {
    27             //    new byte[]{7, 0, 0, 5, 0, 0, 0, 0, 2},
    28             //    new byte[]{0, 3, 0, 0, 0, 4, 6, 0, 0},
    29             //    new byte[]{0, 0, 2, 6, 0, 0, 0, 0, 0},
    30             //    new byte[]{2, 0, 0, 0, 7, 0, 0, 0, 5},
    31             //    new byte[]{5, 0, 0, 1, 0, 3, 0, 0, 6},
    32             //    new byte[]{3, 0, 0, 4, 0, 0, 0, 0, 9},
    33             //    new byte[]{0, 0, 0, 0, 0, 1, 5, 0, 0},
    34             //    new byte[]{0, 0, 7, 2, 0, 0, 0, 4, 0},
    35             //    new byte[]{4, 0, 0, 0, 0, 9, 0, 0, 7},};
    36             //全部要靠尝试来填
    37             byte[][] game = new byte[][] {
    38                 new byte[]{1, 0, 0, 2, 0, 0, 3, 0, 0},
    39                 new byte[]{0, 4, 0, 5, 0, 0, 0, 6, 0},
    40                 new byte[]{0, 0, 0, 7, 0, 0, 8, 0, 0},
    41                 new byte[]{3, 0, 0, 0, 0, 7, 0, 0, 0},
    42                 new byte[]{0, 9, 0, 0, 0, 0, 0, 5, 0},
    43                 new byte[]{0, 0, 0, 6, 0, 0, 0, 0, 7},
    44                 new byte[]{0, 0, 2, 0, 0, 4, 0, 0, 0},
    45                 new byte[]{0, 5, 0, 0, 0, 6, 0, 9, 0},
    46                 new byte[]{0, 0, 8, 0, 0, 1, 0, 0, 3},};
    47             var su = new SudokuSolver(game);
    48             var r = su.Solve();
    49             var r1 = r.First();
    50             static IEnumerable<PathTree> GetPath(PathTree pathTree)
    51             {
    52                 List<PathTree> list = new List<PathTree>();
    53                 var path = pathTree;
    54                 while (path.Parent != null)
    55                 {
    56                     list.Add(path);
    57                     path = path.Parent;
    58                 }
    59 
    60                 return list.Reverse<PathTree>();
    61             }
    62 
    63             var p = GetPath(r1.path).Select(x => $"在 {x.X + 1} 行 {x.Y + 1} 列填入 {x.Number}");
    64             foreach(var step in p)
    65             {
    66                 Console.WriteLine(step);
    67             }
    68 
    69             Console.WriteLine(r1.sudoku);
    70             Console.ReadKey();
    71         }
    View Code

    结果预览:

           上面还有,更多步骤,太长,就不全部截下来了。关于第二张图详情请看后面的总结部分。

    总结

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

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

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

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

      完整源代码:Github

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

  • 相关阅读:
    删除重复数据
    jquery删除文件
    统计目录下文件数及大小
    koa generator
    如何做单测? 单测和开发占比应该是多少?集成测试
    webpack之loader和plugin简介
    服务端渲染
    请求头包含哪些部分
    vue的高阶组件
    amd,cmd规范
  • 原文地址:https://www.cnblogs.com/coredx/p/12173702.html
Copyright © 2020-2023  润新知