1. 程式人生 > 程式設計 >C# 數獨求解演算法的實現

C# 數獨求解演算法的實現

前言

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

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

正文

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

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

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

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

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

數獨求解器

public class SudokuSolver
 {
  /// <summary>
  /// 題目面板
  /// </summary>
  public SudokuBlock[][] SudokuBoard { get; }

  public SudokuSolver(byte[][] board)
  {
   SudokuBoard = new SudokuBlock[board.Length][];
   //初始化數獨的行
   for (int i = 0; i < board.Length; i++)
   {
    SudokuBoard[i] = new SudokuBlock[board[i].Length];
    //初始化每行的列
    for (int j = 0; j < board[i].Length; j++)
    {
     SudokuBoard[i][j] = new SudokuBlock(
      board[i][j] > 0,board[i][j] <= 0 ? new BitArray(board.Length) : null,board[i][j] > 0 ? (byte?)board[i][j] : null,(byte)i,(byte)j);
    }
   }
  }

  /// <summary>
  /// 求解數獨
  /// </summary>
  /// <returns>獲得的解</returns>
  public IEnumerable<(SudokuState sudoku,PathTree path)> Solve(bool multiAnswer = false)
  {
   //初始化各個單元格能填入的數字
   InitCandidate();

   var pathRoot0 = new PathTree(null,-1,-1); //填寫路徑樹,在非遞迴方法中用於記錄回退路徑和其他有用資訊,初始生成一個根
   var path0 = pathRoot0;

   //迴圈填入能填入的數字只有一個的單元格,每次填入都可能產生新的唯一數單元格,直到沒有唯一數單元格可填
   while (true)
   {
    if (!FillUniqueNumber(ref path0))
    {
     break;
    }
   }

   //檢查是否在填唯一數單元格時就已經把所有單元格填滿了
   var finish = true;
   foreach (var row in SudokuBoard)
   {
    foreach (var cell in row)
    {
     if (!cell.IsCondition && !cell.IsUnique)
     {
      finish = false;
      break;
     }
    }
    if (!finish)
    {
     break;
    }
   }
   if (finish)
   {
    yield return (new SudokuState(this),path0);
    yield break;
   }

   var pathRoot = new PathTree(null,-1); //填寫路徑樹,在非遞迴方法中用於記錄回退路徑和其他有用資訊,初始生成一個根
   var path = pathRoot;
   var toRe = new List<(SudokuState sudoku,PathTree path)>();
   //還存在需要試數才能求解的單元格,開始暴力搜尋
   int i = 0,j = 0;
   while (true)
   {
    (i,j) = NextBlock(i,j);

    //正常情況下返回-1表示已經全部填完
    if (i == -1 && j == -1 && !multiAnswer)
    {
     var pathLast = path;//記住最後一步
     var path1 = path;
     while(path1.Parent.X != -1 && path1.Parent.Y != -1)
     {
      path1 = path1.Parent;
     }

     //將暴力搜尋的第一步追加到唯一數單元格的填寫步驟的最後一步之後,連線成完整的填數步驟
     path0.Children.Add(path1);
     path1.Parent = path0;
     yield return (new SudokuState(this),pathLast);
     break;
    }

    var numNode = path.Children.LastOrDefault();
    //確定要從哪個數開始進行填入嘗試
    var num = numNode == null
     ? 0
     : numNode.Number;

    bool filled = false; //是否發現可以填入的數
    //迴圈檢視從num開始接下來的候選數是否能填(num是最後一次填入的數,傳到Candidate[]的索引器中剛好指向 num + 1是否能填的儲存位,對於標準數獨,候選數為 1~9,Candidate的索引範圍就是 0~8)
    for (; !SudokuBoard[i][j].IsCondition && !SudokuBoard[i][j].IsUnique && num < SudokuBoard[i][j].Candidate.Length; num++)
    {
     //如果有可以填的候選數,理論上不會遇見沒有可以填的情況,這種死路情況已經在UpdateCandidate時檢查了
     if (SudokuBoard[i][j].Candidate[num] && !path.Children.Any(x => x.Number - 1 == num && !x.Pass))
     {
      filled = true; //進來了說明單元格有數可以填
      //記錄步驟
      var node = new PathTree(SudokuBoard[i][j],i,j,num + 1,path);
      path = node;
      //如果更新相關單元格的候選數時發現死路(更新函式會在發現死路時自動撤銷更新)
      (bool canFill,(byte x,byte y)[] setList) updateResult = UpdateCandidate(i,(byte)(num + 1));
      if (!updateResult.canFill)
      {
       //記錄這條路是死路
       path.SetPass(false);
      }
      //僅在確認是活路時設定填入數字
      if (path.Pass)
      {
       SudokuBoard[i][j].SetNumber((byte)(num + 1));
       path.SetList = updateResult.setList;//記錄相關單元格可填數更新記錄,方便在回退時撤銷更新
      }
      else //出現死路,要進行回退,重試這個單元格的其他可填數字
      {
       path.Block.SetNumber(null);
       path = path.Parent;
      }
      //填入一個候選數後跳出迴圈,不再繼續嘗試填入之後的候選數
      break;
     }
    }
    if (!filled)//如果沒有成功填入數字,說明上一步填入的單元格就是錯的,會導致後面的單元格怎麼填都不對,要回退到上一個單元格重新填
    {
     path.SetPass(false);
     path.Block.SetNumber(null);
     foreach (var pos in path.SetList)
     {
      SudokuBoard[pos.x][pos.y].Candidate.Set(path.Number - 1,true);
     }
     path = path.Parent;
     i = path.X < 0 ? 0 : path.X;
     j = path.Y < 0 ? 0 : path.Y;
    }
   }
  }

  /// <summary>
  /// 初始化候選項
  /// </summary>
  private void InitCandidate()
  {
   //初始化每行空缺待填的數字
   var rb = new List<BitArray>();
   for (int i = 0; i < SudokuBoard.Length; i++)
   {
    var r = new BitArray(SudokuBoard.Length);
    r.SetAll(true);
    for (int j = 0; j < SudokuBoard[i].Length; j++)
    {
     //如果i行j列是條件(題目)給出的數,設定數字不能再填(r[x] == false 表示 i 行不能再填 x + 1,下標加1表示數獨可用的數字,下標對應的值表示下標加1所表示的數是否還能填入該行)
     if (SudokuBoard[i][j].IsCondition || SudokuBoard[i][j].IsUnique)
     {
      r.Set(SudokuBoard[i][j].Number.Value - 1,false);
     }
    }
    rb.Add(r);
   }

   //初始化每列空缺待填的數字
   var cb = new List<BitArray>();
   for (int j = 0; j < SudokuBoard[0].Length; j++)
   {
    var c = new BitArray(SudokuBoard[0].Length);
    c.SetAll(true);
    for (int i = 0; i < SudokuBoard.Length; i++)
    {
     if (SudokuBoard[i][j].IsCondition || SudokuBoard[i][j].IsUnique)
     {
      c.Set(SudokuBoard[i][j].Number.Value - 1,false);
     }
    }
    cb.Add(c);
   }

   //初始化每宮空缺待填的數字(目前只能算標準 n×n 數獨的宮)
   var gb = new List<BitArray>();
   //n表示每宮應有的行、列數(標準數獨行列、數相同)
   var n = (int)Sqrt(SudokuBoard.Length);
   for (int g = 0; g < SudokuBoard.Length; g++)
   {
    var gba = new BitArray(SudokuBoard.Length);
    gba.SetAll(true);
    for (int i = g / n * n; i < g / n * n + n; i++)
    {
     for (int j = g % n * n; j < g % n * n + n; j++)
     {
      if (SudokuBoard[i][j].IsCondition || SudokuBoard[i][j].IsUnique)
      {
       gba.Set(SudokuBoard[i][j].Number.Value - 1,false);
      }
     }
    }
    gb.Add(gba);
   }

   //初始化每格可填的候選數字
   for (int i = 0; i < SudokuBoard.Length; i++)
   {
    for (int j = 0; j < SudokuBoard[i].Length; j++)
    {

     if (!SudokuBoard[i][j].IsCondition)
     {
      var c = SudokuBoard[i][j].Candidate;
      c.SetAll(true);
      //當前格能填的數為其所在行、列、宮同時空缺待填的數字,按位與運算後只有同時能填的候選數保持1(可填如當前格),否則變成0
      // i / n * n + j / n:根據行號列號計算宮號,
      c = c.And(rb[i]).And(cb[j]).And(gb[i / n * n + j / n]);
      SudokuBoard[i][j].SetCandidate(c);
     }
    }
   }
  }

  /// <summary>
  /// 求解開始時尋找並填入單元格唯一可填的數,減少解空間
  /// </summary>
  /// <returns>是否填入過數字,如果為false,表示能立即確定待填數字的單元格已經沒有,要開始暴力搜尋了</returns>
  private bool FillUniqueNumber(ref PathTree path)
  {
   var filled = false;
   for (int i = 0; i < SudokuBoard.Length; i++)
   {
    for (int j = 0; j < SudokuBoard[i].Length; j++)
    {
     if (!SudokuBoard[i][j].IsCondition && !SudokuBoard[i][j].IsUnique)
     {
      var canFillCount = 0;
      var index = -1;
      for (int k = 0; k < SudokuBoard[i][j].Candidate.Length; k++)
      {
       if (SudokuBoard[i][j].Candidate[k])
       {
        index = k;
        canFillCount++;
       }
       if (canFillCount > 1)
       {
        break;
       }
      }
      if (canFillCount == 0)
      {
       throw new Exception("有單元格無法填入任何數字,數獨無解");
      }
      if (canFillCount == 1)
      {
       var num = (byte)(index + 1);
       SudokuBoard[i][j].SetNumber(num);
       SudokuBoard[i][j].SetUnique();
       filled = true;
       var upRes = UpdateCandidate(i,num);
       if (!upRes.canFill)
       {
        throw new Exception("有單元格無法填入任何數字,數獨無解");
       }
       path = new PathTree(SudokuBoard[i][j],num,path);
       path.SetList = upRes.setList;
      }
     }
    }
   }
   return filled;
  }

  /// <summary>
  /// 更新單元格所在行、列、宮的其它單元格能填的數字候選,如果沒有,會撤銷更新
  /// </summary>
  /// <param name="row">行號</param>
  /// <param name="column">列號</param>
  /// <param name="canNotFillNumber">要剔除的候選數字</param>
  /// <returns>更新候選數後,所有被更新的單元格是否都有可填的候選數字</returns>
  private (bool canFill,byte y)[] setList) UpdateCandidate(int row,int column,byte canNotFillNumber)
  {
   var canFill = true;
   var list = new List<SudokuBlock>(); // 記錄修改過的單元格,方便撤回修改

   bool CanFillNumber(int i,int j)
   {
    var re = true;
    var _canFill = false;
    for (int k = 0; k < SudokuBoard[i][j].Candidate.Length; k++)
    {
     if (SudokuBoard[i][j].Candidate[k])
     {
      _canFill = true;
      break;
     }
    }
    if (!_canFill)
    {
     re = false;
    }

    return re;
   }
   bool Update(int i,int j)
   {
    if (!(i == row && j == column) && !SudokuBoard[i][j].IsCondition && !SudokuBoard[i][j].IsUnique && SudokuBoard[i][j].Candidate[canNotFillNumber - 1])
    {
     SudokuBoard[i][j].Candidate.Set(canNotFillNumber - 1,false);
     list.Add(SudokuBoard[i][j]);

     return CanFillNumber(i,j);
    }
    else
    {
     return true;
    }
   }

   //更新該行其餘列
   for (int j = 0; j < SudokuBoard[row].Length; j++)
   {
    canFill = Update(row,j);
    if (!canFill)
    {
     break;
    }
   }

   if (canFill) //只在行更新時沒發現無數可填的單元格時進行列更新才有意義
   {
    //更新該列其餘行
    for (int i = 0; i < SudokuBoard.Length; i++)
    {
     canFill = Update(i,column);
     if (!canFill)
     {
      break;
     }
    }
   }

   if (canFill)//只在行、列更新時都沒發現無數可填的單元格時進行宮更新才有意義
   {
    //更新該宮其餘格
    //n表示每宮應有的行、列數(標準數獨行列、數相同)
    var n = (int)Sqrt(SudokuBoard.Length);
    //g為宮的編號,根據行號列號計算
    var g = row / n * n + column / n;
    for (int i = g / n * n; i < g / n * n + n; i++)
    {
     for (int j = g % n * n; j < g % n * n + n; j++)
     {
      canFill = Update(i,j);
      if (!canFill)
      {
       goto canNotFill;
      }
     }
    }
    canNotFill:;
   }

   //如果發現存在沒有任何數字可填的單元格,撤回所有候選修改
   if (!canFill)
   {
    foreach (var cell in list)
    {
     cell.Candidate.Set(canNotFillNumber - 1,true);
    }
   }

   return (canFill,list.Select(x => (x.X,x.Y)).ToArray());
  }

  /// <summary>
  /// 尋找下一個要嘗試填數的格
  /// </summary>
  /// <param name="i">起始行號</param>
  /// <param name="j">起始列號</param>
  /// <returns>找到的下一個行列號,沒有找到返回-1</returns>
  private (int x,int y) NextBlock(int i = 0,int j = 0)
  {
   for (; i < SudokuBoard.Length; i++)
   {
    for (; j < SudokuBoard[i].Length; j++)
    {
     if (!SudokuBoard[i][j].IsCondition && !SudokuBoard[i][j].IsUnique && !SudokuBoard[i][j].Number.HasValue)
     {
      return (i,j);
     }
    }
    j = 0;
   }

   return (-1,-1);
  }

  public override string ToString()
  {
   static string Str(SudokuBlock b)
   {
    var n1 = new[] { "①","②","③","④","⑤","⑥","⑦","⑧","⑨" };
    var n2 = new[] { "⑴","⑵","⑶","⑷","⑸","⑹","⑺","⑻","⑼" };
    return b.Number.HasValue
     ? b.IsCondition
      ? " " + b.Number
      : b.IsUnique
       ? n1[b.Number.Value - 1]
       : n2[b.Number.Value - 1]
     : "▢";
   }
   return
$@"{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])}
{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])}
{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])}
{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])}
{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])}
{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])}
{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])}
{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])}
{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])}";
  }
 }

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

求解過程資訊

public class PathTree
 {
  public PathTree Parent { get; set; }
  public List<PathTree> Children { get; } = new List<PathTree>();

  public SudokuBlock Block { get; }
  public int X { get; }
  public int Y { get; }
  public int Number { get; }
  public bool Pass { get; private set; } = true;
  public (byte x,byte y)[] SetList { get; set; }

  public PathTree(SudokuBlock block,int x,int y,int number)
  {
   Block = block;
   X = x;
   Y = y;
   Number = number;

  }

  public PathTree(SudokuBlock block,int row,int number,PathTree parent)
   : this(block,row,column,number)
  {
   Parent = parent;
   Parent.Children.Add(this);
  }

  public void SetPass(bool pass)
  {
   Pass = pass;
  }
 }

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

答案儲存

public class SudokuState
 {
  public SudokuBlock[][] SudokuBoard { get; }
  public SudokuState(SudokuSolver sudoku)
  {
   SudokuBoard = new SudokuBlock[sudoku.SudokuBoard.Length][];
   //初始化數獨的行
   for (int i = 0; i < sudoku.SudokuBoard.Length; i++)
   {
    SudokuBoard[i] = new SudokuBlock[sudoku.SudokuBoard[i].Length];
    //初始化每行的列
    for (int j = 0; j < sudoku.SudokuBoard[i].Length; j++)
    {
     SudokuBoard[i][j] = new SudokuBlock(
      sudoku.SudokuBoard[i][j].IsCondition,null,sudoku.SudokuBoard[i][j].Number,(byte)j);
     if (sudoku.SudokuBoard[i][j].IsUnique)
     {
      SudokuBoard[i][j].SetUnique();
     }
    }
   }
  }

  public override string ToString()
  {
   static string Str(SudokuBlock b)
   {
    var n1 = new[] { "①",{Str(SudokuBoard[8][8])}";
  }
 }

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

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

 public class SudokuBlock
 {
  /// <summary>
  /// 填入的數字
  /// </summary>
  public byte? Number { get; private set; }

  /// <summary>
  /// X座標
  /// </summary>
  public byte X { get; }

  /// <summary>
  /// Y座標
  /// </summary>
  public byte Y { get; }

  /// <summary>
  /// 候選數字,下標所示狀態表示數字“下標加1”是否能填入
  /// </summary>
  public BitArray Candidate { get; private set; }

  /// <summary>
  /// 是否為條件(題目)給出數字的單元格
  /// </summary>
  public bool IsCondition { get; }

  /// <summary>
  /// 是否為遊戲開始就能確定唯一可填數字的單元格
  /// </summary>
  public bool IsUnique { get; private set; }

  public SudokuBlock(bool isCondition,BitArray candidate,byte? number,byte x,byte y)
  {
   IsCondition = isCondition;
   Candidate = candidate;
   Number = number;
   IsUnique = false;
   X = x;
   Y = y;
  }

  public void SetNumber(byte? number)
  {
   Number = number;
  }

  public void SetCandidate(BitArray candidate)
  {
   Candidate = candidate;
  }

  public void SetUnique()
  {
   IsUnique = true;
  }
 }

測試程式碼

static void Main(string[] args)
  {
   //模板
   //byte[][] game = new byte[][] {
   // new byte[]{0,0},// new byte[]{0,};
   //這個簡單,無需嘗試,一直填唯一數單元格,填完後剩下的單元格又有會變唯一數單元格
   //byte[][] game = new byte[][] {
   // new byte[]{0,5,7,6,1,8,9,3,4,6},// new byte[]{6,5},// new byte[]{9,2,};
   //可以填一部分唯一數單元格,剩下一部分需要嘗試,除錯用
   //byte[][] game = new byte[][] {
   // new byte[]{7,2},// new byte[]{2,// new byte[]{5,// new byte[]{3,9},// new byte[]{4,7},};
   //全部要靠嘗試來填
   byte[][] game = new byte[][] {
    new byte[]{1,new byte[]{0,new byte[]{3,3},};
   var su = new SudokuSolver(game);
   var r = su.Solve();
   var r1 = r.First();
   static IEnumerable<PathTree> GetPath(PathTree pathTree)
   {
    List<PathTree> list = new List<PathTree>();
    var path = pathTree;
    while (path.Parent != null)
    {
     list.Add(path);
     path = path.Parent;
    }

    return list.Reverse<PathTree>();
   }

   var p = GetPath(r1.path).Select(x => $"在 {x.X + 1} 行 {x.Y + 1} 列填入 {x.Number}");
   foreach(var step in p)
   {
    Console.WriteLine(step);
   }

   Console.WriteLine(r1.sudoku);
   Console.ReadKey();
  }

結果預覽:

上面還有,更多步驟,太長,就不全部截下來了。關於第二張圖詳情請看後面的總結部分。

總結

這個數獨求解器運用了大量 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

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。