65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 C# Windows 应用程序的数独求解器

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2021 年 9 月 25 日

CPOL

7分钟阅读

viewsIcon

15446

downloadIcon

782

数独问题编码解决方案概述

引言

数独是一款适合所有年龄段的有趣谜题,而且很容易获得。一旦我对数独产生了兴趣,我就一直想写一个程序来解决数独谜题。我曾短暂地在网上搜索过数独求解器,但它们大多基于暴力破解方法。我想以我手动解决问题的方式来编写代码。过去我曾尝试过两次,但只取得了部分成功。然后,在第三次尝试中,我设法将其完成到合理的程度。首先,这个程序一开始并没有进行任何猜测。如果它无法解决谜题,那么我将尝试使用获得的可能值来使用暴力破解方法解决。

背景

您需要对解决数独的基础知识有所了解。当您无需猜测即可解决代码时,该解决方案就能在很大程度上解决谜题。对于未解决的谜题,您可以使用日志找出可能的数值,这些数值可以作为输入进行进一步尝试。我已附上应用程序 (EXE) 的完整可运行版本和源代码,供您进一步开发。

Using the Code

在将解决方案移植到另一台计算机后,我遇到了由于 DPI 问题导致的窗体渲染问题。因此,我不得不使用以下代码来解决它。没有这些行,Winform 在不同的机器上看起来会很模糊。

static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            // ***this line is added***
            if (Environment.OSVersion.Version.Major >= 6)
                SetProcessDPIAware();

            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new SudokuCSharp.Form1());
        }

        // ***also dllimport of that function***
        [System.Runtime.InteropServices.DllImport("user32.dll")]
        private static extern bool SetProcessDPIAware();
    }

现在进入编码部分,我的解决方法如下:

为了存储问题陈述,我创建了一个结构,如下所示:

 private struct mStruct
        {
            public bool isLocked;  //To know whether it can be altered or not. 
                                   //If Locked, then it cannot be altered
            public int curValue;   //current value
            public bool isSource;  //To know whether this is the source data or not. 
            public string posValue;//to list down possible values it can hold
            public int quarter;    //to know which quarter the array belongs
            public string rem;     //remarks columns
        }

然后,我创建了一个由该结构组成的数组来存储问题陈述。此处解释了 mstruct 的字段:

  • isLocked:布尔类型 - 用于了解值是否不能被修改。这可以是初始问题的一部分,也可以是迭代后找到的正确值。因此,一旦固定,就没有选项可以更改。
  • curvalue:整数 - 单元格中的当前值。
  • isSource:布尔类型 - 如果为 true,则表示来自问题陈述。如果为 false,则表示待找到的值。用于区分单元格值是来自问题陈述还是找到的值。
  • posvaluestring - 包含单元格可以保存的所有可能值的逗号分隔列表。
  • quarter:整数 - 我将整个 9x9 的单元格分割成 9 个区域。此值指示单元格属于哪个区域。
  • rem:这是一个 string,用于保存任何备注。

我使用上述 struct 创建了一个 9x9 的数组,并将问题陈述存储在其中。我称此数组为 playArray,因为我将使用它来寻找答案。

我还做了一个简单的方法来识别来自文本框控件名称的单元格。所有 textbox 控件都命名为:'txtq<quarter>r<row no>c<col no>'。例如,在文本框名称 'txtq2r0c7' 中 - txt 表示这是一个文本框,接下来的 2 个字母 'q2' 表示它属于哪个区域 - 这里是第二个区域。接下来的 4 个字母 'r0c7' 表示行和列的位置 - 第 0 行和第 7 列。通过这种方式,我可以识别值的来源,并且还可以轻松地将值赋回 textbox。请参阅下面的图片了解 textbox 的命名和区域命名。

将区域划分为四个区域的原因是为了解决每个区域的 3x3 单元格。如您所知,简单的数独规则是:一行、一列和一个区域中不能重复出现 1-9 的数字。

加载问题陈述

输入问题中的单元格值有些困难,因为我们需要使用 Tab 键在 textbox 之间移动。因此,我想出了一个简单的方法来做到这一点 - 在没有值的单元格中输入 0。

例如,问题陈述:000500809180400060000070010500000070649000000020010000090032000010005600700000480 加载如下。值是按行输入的,所有 0 都被假定为空白网格。这有助于更快地加载问题。

解决谜题

该谜题被迭代尝试,直到找到解决方案。关键函数是:

SolveHorizontalVerticalOrQuadrant("q");  //Solve for Quadrant
SolveHorizontalVerticalOrQuadrant("r");  //Solve for Row
SolveHorizontalVerticalOrQuadrant("c");  //Solve for Col

首先按区域、然后按行、最后按列进行解决。请注意,函数名称相同,只是参数 'q'、'r'、'c' 不同,以区分我们是按区域、按行还是按列进行求解。逻辑保持不变,只是根据发送的参数,相应地选择并处理数组。

一旦我知道数组中的值列表,我就会识别出没有值的单元格(需要解决的单元格)。然后使用以下方法为这些单元格分配所有可能的值。首先,它可以包含 1 到 9 的所有值。然后,我迭代剩余的有值的单元格,并从该单元格的可能值中删除这些值。

private readonly string possSolution = @"123456789"; //string that holds all possible values 
                                                     //an empty cell can have

以下代码确定了单元格可以保存的所有可能值:

 //Get possible solution in a line
                for (int j = 0; j < noOfElementsInRowOrCol; j++)
                {
                    if (lStruct[j].curValue > 0)
                    {
                        lPossSolution = lPossSolution.Replace(lStruct[j].curValue.ToString(), 
                                        "");//remove the values from possible solution
                    }
                    else
                    {
                        //Get the row and col where the value is 0
                        row = Int32.Parse(lStruct[j].rem.Substring(1, 1));//assuming r1c1
                        col = Int32.Parse(lStruct[j].rem.Substring(3, 1));//
                        zeroCells[k] = row * 10 + col;
                        k += 1;
                    }
                }

有两种方法可以确定空单元格的正确值:

如果根据其余单元格只有一个可能值,或者如果可能值中只有一个唯一值(如果一个单元格的可能值如下:根据其余单元格为 1,2,3,2,3,这清楚地表明它可以是 1 或 2 或 3。由于 1 只出现一次,这清楚地表明它可以是该值)。这是通过以下函数实现的:

        /// <summary>
        /// All possible values are sent here from zero cells. 
        /// This checks for unique value and returns it which is the correct value
        /// </summary>
        /// <param name="lStr"></param>
        private void GetUniqueValue(string lStr)
        {
            Array.Clear(lIntArray, 0, lIntArray.Length);//Clear the array
            Array.Clear(ldoubleIntArray, 0, ldoubleIntArray.Length);//Clear the array
            Array.Clear(ltripleIntArray, 0, ltripleIntArray.Length);//Clear the array

            int i = 0;
            int j = 0;
            int k = 0;
            Dictionary<char, int> dict = new Dictionary<char, int>();

            try
            {
                foreach (char ch in lStr)
                {
                    if (dict.ContainsKey(ch))
                        dict[ch] = dict[ch] + 1;
                    else
                        dict.Add(ch, 1);
                }
                foreach (KeyValuePair<char, int> m in dict)
                {
                    if (m.Value == 1)// check for single occurence
                    {
                        //unique values in array is possible
                        if (m.Key.ToString() != ":")
                        {
                            lIntArray[i] = Int32.Parse(m.Key.ToString());
                            i++;
                        }
                    }
                    else if (m.Value == 2)//check for double occurence
                    {
                        if (m.Key.ToString() != ":")
                        {
                            ldoubleIntArray[j] = Int32.Parse(m.Key.ToString());
                            j++;
                        }
                    }
                    else if (m.Value == 3)//check for triple occurence
                    {
                        if (m.Key.ToString() != ":")
                        {
                            ltripleIntArray[k] = Int32.Parse(m.Key.ToString());
                            k++;
                        }
                    }
                }
            }
            catch (Exception e)
            {
                HasChanged = false;
                throw e;
            }
        }

使用以下两种方法更新空单元格上的可能值:

UpdatePossValues(lvalue.ToString(), row.ToString(), col, false);          //To update the row
UpdatePossValues(lvalue.ToString(), col.ToString(), row, true);           //To update the col
UpdatePossValuesInQuarter(lvalue.ToString(), playArray[row, col].quarter);//To update in a 
                                                                          //quarter

每次迭代后的可能值会在此处打印在富文本框中。请查看 if 条件 - isSource 字段用于选择待解决单元格的可能值。

/// <summary>
/// To print possible solution in the Rich text boxes
/// </summary>
        private void PrintPossSolution()
        {
            for (int i = 0; i < noOfElementsInRowOrCol; i++)
            {
                for (int j = 0; j < noOfElementsInRowOrCol; j++)
                {
                    if (!playArray[i, j].isSource)
                    {
                        richTextBox1.AppendText("R" + i.ToString() + "C" + 
                        j.ToString() + ": " + playArray[i, j].posValue + Environment.NewLine);
                    }
                }
            }
        }

我还添加了一些额外的方法,如果常规的解决方法不能给出完整答案,则可以使用这些方法进行解决。

SolvePossibleValuesRow2();  //check in Row for possible values with 2 occurences
SolvePossibleValuesCol2();  //check in Col for possible values with 2 occurences

例如,假设在 Q3 中,单元格 r4c0r4c1 的可能值都是 2,3,那么这就表明 2 和 3 只能出现在该区域的第 4 行,而不能出现在该区域的任何其他行。有了这个线索,这两个数字就可以从该区域的 r3 和 r5 中删除。通过减少可能值,这将解决其他空单元格。同样的方法也适用于 column。这些被称为裸对。

对于一个区域中的 3 个单元格,按行和按列解决的方案可以进一步扩展,这为进一步开发提供了空间。

此处显示了一个完全解出的谜题。每行和每列后面的数字 45 显示了该行和列的计数。这用于检查程序是否已解决解决方案。

日志显示了解决迭代次数。此外,对于每个待解决的单元格,它会显示可能的数值。在这种情况下,由于已解决,每个单元格只有一个值。在程序无法解决的情况下,它会显示可能的数值 - 例如,R0C1: 5,6,7,这意味着 R0C1 可以是 5 或 6 或 7。

关注点

文本框的命名约定是获取值并将其赋回文本框的一种好方法。有关裸对的更多信息,请参阅以下链接:

历史

  • 2020 年 6 月 8 日:版本 1.0 - 常规解决方法
  • 2020 年 6 月 29 日:版本 1.0(无更改)- 实现隐藏裸对作为可能解决方案
  • 2021 年 2 月 20 日:版本 1.1 - 实现暴力破解方法
© . All rights reserved.