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, -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, -1, -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, j, (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, j, num);
                        if (!upRes.canFill)
                        {
                            throw new Exception("有單元格無法填入任何數字,數獨無解");
                        }
                        path = new PathTree(SudokuBoard[i][j], 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 x, 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 column, 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)i
                    , (byte)j);
                if (sudoku.SudokuBoard[i][j].IsUnique)
                {
                    SudokuBoard[i][j].SetUnique();
                }
            }
        }
    }

    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])}";
    }
}

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

單元格定義

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, 0, 0, 0, 0, 0, 0, 0},
    //    new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0},
    //    new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0},
    //    new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0},
    //    new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0},
    //    new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0},
    //    new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0},
    //    new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0},
    //    new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0},};
    //這個簡單,無需嘗試,一直填唯一數單元格,填完後剩下的單元格又有會變唯一數單元格
    //byte[][] game = new byte[][] {
    //    new byte[]{0, 5, 0, 7, 0, 6, 0, 1, 0},
    //    new byte[]{0, 8, 0, 0, 9, 0, 0, 6, 0},
    //    new byte[]{0, 6, 9, 0, 8, 0, 7, 3, 0},
    //    new byte[]{0, 1, 0, 0, 4, 0, 0, 0, 6},
    //    new byte[]{6, 0, 7, 1, 0, 3, 8, 0, 5},
    //    new byte[]{9, 0, 0, 0, 0, 8, 0, 2, 0},
    //    new byte[]{0, 2, 4, 0, 1, 0, 6, 5, 0},
    //    new byte[]{0, 7, 0, 0, 6, 0, 0, 4, 0},
    //    new byte[]{0, 9, 0, 4, 0, 2, 0, 8, 0},};
    //可以填一部分唯一數單元格,剩下一部分需要嘗試,調試用
    //byte[][] game = new byte[][] {
    //    new byte[]{7, 0, 0, 5, 0, 0, 0, 0, 2},
    //    new byte[]{0, 3, 0, 0, 0, 4, 6, 0, 0},
    //    new byte[]{0, 0, 2, 6, 0, 0, 0, 0, 0},
    //    new byte[]{2, 0, 0, 0, 7, 0, 0, 0, 5},
    //    new byte[]{5, 0, 0, 1, 0, 3, 0, 0, 6},
    //    new byte[]{3, 0, 0, 4, 0, 0, 0, 0, 9},
    //    new byte[]{0, 0, 0, 0, 0, 1, 5, 0, 0},
    //    new byte[]{0, 0, 7, 2, 0, 0, 0, 4, 0},
    //    new byte[]{4, 0, 0, 0, 0, 9, 0, 0, 7},};
    //全部要靠嘗試來填
    byte[][] game = new byte[][] {
        new byte[]{1, 0, 0, 2, 0, 0, 3, 0, 0},
        new byte[]{0, 4, 0, 5, 0, 0, 0, 6, 0},
        new byte[]{0, 0, 0, 7, 0, 0, 8, 0, 0},
        new byte[]{3, 0, 0, 0, 0, 7, 0, 0, 0},
        new byte[]{0, 9, 0, 0, 0, 0, 0, 5, 0},
        new byte[]{0, 0, 0, 6, 0, 0, 0, 0, 7},
        new byte[]{0, 0, 2, 0, 0, 4, 0, 0, 0},
        new byte[]{0, 5, 0, 0, 0, 6, 0, 9, 0},
        new byte[]{0, 0, 8, 0, 0, 1, 0, 0, 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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章