[摘要]
本文介绍了一个能够求解数独问题的求解器的设计与实现,该求解器具备多种功能。它可以通过识别图片中的数独网格,也可以随机生成数独题目,识别数独题目;对数独进行验证、提示,并能展示求解过程的算法演示。
该求解器有两种生产数独题目的方式。第一种是随机生成:在题库中先预存好大量的数独题目,然后用随机函数随机生成数独题目。第二种是图片识别:利用全连接神经网络建立图片数字识别模型,用户点击图片识别开始游戏后,程序将图像切割为81个小块,将分割后的小块进行归一化、灰度化处理,预处理和提取后,然后将其输入求解器进行求解。
为了求解数独题目,求解器采用了一种基于回溯的高效算法。它通过不断地给空单元格分配数字并在遇到矛盾时回溯,系统地探索可能的解决方案。该求解器保证能够找到有效数独题目的正确解。
对于数独的验证和提示,求解器通过验证每行、每列和每个3x3子网格中的数字唯一性来检查给定的题目是否符合数独规则。这个功能可以帮助用户验证手动输入或生成的数独题目的正确性。
此外,求解器还提供了算法演示模式,展示回溯解题过程的逐步演示。这种模式有助于用户理解回溯算法,并可以作为学习回溯策略的教育工具。
所提出的求解器集数独求解、图像识别、题目生成、验证、提示和算法演示于一体,为数独爱好者提供了一个多功能的工具。实现展示了功能的实用性和有效性,为数独相关任务提供了一个全面的解决方案。
[关键词]
数独求解器、图像识别、题目生成、数独验证、回溯算法、算法演示。
目录
1. 引言
2. 数独页面的创建
3. 数独题目的产生
4. 数独的功能实现
5. 回溯算法运行演示
- 引言
数独 (Sudoku) 是一种逻辑智力游戏, 起源于瑞士数学家欧拉等人研究的拉丁方阵 (Latin Square) , 目前风靡全球。随着数独爱好者日益增多, 数独也出现了许多变形, 比如迷你数独、对角线数独, 其规则种类数不胜数。
数独由方格、行、列、宫等元素组成, 是将一个较大的正方形均分成9行9列, 形成81个方格, 也称九宫格, 每个方格都是一个小正方形, 水平方向的三横行与垂直方向的三纵列相交之处有9个方格, 构成一个小九宫, 称为宫。数独的游戏规则是:在方格中填入1到9的数字, 并满足每一行只能填充1~9的数字, 且不能重复;每一列只能填充1~9的数字, 且不能重复;在每3×3的方格中填充1~9的数字, 且不能重复。
- 数独页面的创建
- 代码实现
图1-1 tk页面渲染代码
这段代码实现了一个基于Tkinter的数独求解器的图形用户界面(GUI)。下面是代码的实现逻辑:
1. 在`__init__`方法中,初始化数独求解器的初始盘面`board`和Tkinter的根窗口`root`。设置 窗口的标题和大小,并创建输入框和输出框的框架。
2. `create_start`方法用于创建数独盘面的输入框。通过嵌套的循环,创建一个9x9的网格, 每个单元格对应一个输入框。设置输入框的宽度、对齐方式和填充,并将其放置在输入框框架 中。
3. `create_input_widgets`方法用于创建用于输入数独的标签和输入框。与`create_start`类 似,但是根据初始盘面`board`的值来确定是创建输入框还是标签。如果初始盘面的值为0,则创 建输入框;否则,创建标签来显示初始盘面的值。
4. `create_output_widgets`方法用于创建用于显示状态或结果的标签和按钮。创建了以下几 个部分的组件:
- `status_label`:显示数独求解器的状态。
- `next_button`:开始一个新的游戏。
- `pic_button`:通过图片识别开始游戏。
- `solve_button`:查看数独的解答。
- `step_button`:提示数独的下一步。
- `show_button`:展示算法的运行过程。
- `verify_button`:验证数独的解答是否正确。
在`SudokuSolverGUI`类中,通过调用`create_output_widgets`和`create_start`方法来创建GUI界面的各个部分。同时,还定义了一些处理按钮点击事件的方法,例如`next_game`、`pic_game`、`solve_sudoku`等。
通过这个GUI界面,用户可以输入数独问题,查看解答、提示和验证解答的正确性等操作。
图1-2 初始化的tk界面
- 数独的题目产生
- 随机开始游戏
1.1原理
为了实现随机生成数独题目的功能,本程序采用了以下步骤。首先,从预设的题库文件中加载题目,题库文件包含了多个数独题目的表示。然后,通过生成一个随机索引,从题库中随机选择一个题目作为当前的数独题目。所选题目以二维列表的形式表示,并作为程序的输入。
1.2代码实现
图1-3 随机生成代码
这段代码包含了一个主函数`main`和一个辅助函数`load_puzzles`。下面是对这两部分代码的解释:
1. `load_puzzles`函数:
- 此函数用于从题库文件中加载题目。题库文件名为"题库",函数通过使用`open`函数打开文件,并使用`readlines`方法读取所有行。
- 然后,对每一行使用`strip`方法去除首尾空白字符,并将题目字符串添加到名为`puzzles`的列表中。
- 最后,返回包含所有题目的`puzzles`列表。
2. `main`函数:
- 首先,调用`load_puzzles`函数加载题目并将其存储在`puzzles`列表中。
- 然后,使用`random.randint`函数随机选择一个索引,该索引用于从`puzzles`列表中选择一个题目。
- 选定的题目通过索引`random_index`获取,并存储在`selected_puzzle`变量中。
- `selected_puzzle`是一个字符串表示的数独题目。
- 接下来,使用`eval`函数将`selected_puzzle`转换为二维列表形式的数独盘面,存储在`board`变量中。
- 最后,创建一个名为`gui`的`SudokuSolverGUI`实例,并调用`run`方法运行界面程序。
图1-4 题库
图1-5 渲染完成后tk界面
2.图片识别开始游戏
为了实现随机生成数独题目的功能,本程序采用了以下步骤。
2.1 训练数字识别模型:
2.1.1 寻找训练模型的框架
经过大量的对比分析,我最终选用PyTorch框架。原因如下:第一,PyTorch提供了简洁而直观的API,使得定义、训练和评估深度学习模型变得更加容易。相比于其他深度学习框架,PyTorch的代码可读性更高,更贴近Python的编程风格,降低了入门门槛;第二,PyTorch采用动态图机制,这意味着可以在运行时动态地定义、修改和调试计算图。这种灵活性使得模型的构建和调试更加直观和灵活。
2.1.2寻找合适数据集
训练模型离不开前期数据集的准备,经过我的再三的比对权衡,最终选择使用MNIST 数据集(手写数字数据集)。原因如下:第一,mnist是一个公开的公共数据集,本身样本量大(60000个训练样本和10000个测试样本,每个样本的像素大小为28*28);第二,mnist数据集的样本为手写体数字,相对于书面体,手写体的识别难度很大,特征值更难提取,如果模型能识别手写体,那么书面体也一定能准确识别。
2.1.3训练模型
用PyTorch实现了一个简单的多层感知器(MLP)模型,用于进行MNIST手写数字识别。
1. 模型定义:定义了一个包含三个线性层(全连接层)的多层感知器模型(Net)。每个线性层后面跟着一个激活函数(ReLU),最后一个线性层的输出作为模型的输出。
2. 模型训练:定义了损失函数(交叉熵损失函数)和优化器(随机梯度下降法),通过迭代训练来更新模型参数。训练过程中,将训练数据输入模型进行前向传播,计算损失值,然后进行反向传播并更新模型参数。
3. 模型评估:使用测试集对训练好的模型进行评估,计算模型在测试集上的平均损失和准确率。同时,对部分测试集样本进行可视化展示,展示模型的预测结果。
当我训练200次后,模型的准确度大约为0.95左右。我想再提升一下准确度,然后将模型训练次数调整为300次,但调整以后模型的准确度只有0.93。我决定暂时先用该模型,如果后续不能准确识别再进行模型的调参,提升其准确度(后来事实证明该模型的准确度可以满足本程序识别数字的要求)。
2.2进行图片处理:
2.2.1图片切割转换
- 使用OpenCV库(`cv2`)读取输入的图片。
- 获取图像的宽度和高度。
- 根据数独格子的数量,计算每个小格子的宽度和高度。
- 循环遍历每个小格子,并通过裁剪、灰度处理和阈值处理等操作,将小格子图像转换为28x28像素的灰度图像(因为mnist数据集的数据为28*28,白字黑背景,所以在这个步骤对原始图片进行处理)。
- 保存每个小格子的图像到指定的文件夹(grid_images)中。
图1-5 图片处理代码
图1-6 处理后的文件
2.2.2 图片识别和数独生成:
- 定义一个函数(`recognize_handwriting`),用于识别手写数字图像,并返回预测的数字标签。
- 定义一个函数(`is_black_background`),用于判断图像是否具有黑色背景。
- 定义一个函数(`get_sudoku_array`),用于从分割后的小格子图像中获取数独的数组表示。
- 定义一个函数(`run_game`),用于处理图片、调用识别函数和生成数独数组。
- 在解答数独时,根据数独求解算法的运行状态,更新界面上的输入框和标签。
图1-4 原始读入的数独图片
图1-5 识别后的tk界面
- 数独的功能实现
4.1利用回溯算法找出数独的解
4.1.1原理
- 定义合法性检查函数:编写一个函数来检查给定的数独状态是否合法。这个函数需要检查每一行、每一列和每个3x3的子网格中是否有重复的数字。如果存在重复,则表示当前状态不合法。
- 找到空白位置:从左上角开始,按照行优先的顺序,找到数独网格中的第一个空白位置(即值为0的位置)。如果没有空白位置,则表示已经找到了一个解。 3. 尝试填充数字:从1到9的数字中选择一个,依次填充到当前的空白位置。然后,检查填充后的状态是否合法。如果合法,则继续下一步;如果不合法,则尝试选择下一个数字。
- 递归回溯:在当前位置填充一个数字后,递归调用回溯函数,移动到下一个空白位置,并继续尝试填充数字。如果在后续的填充过程中找到了不合法的状态,那么回溯到上一个空白位置,尝试选择下一个数字。
- 找到解或回溯完毕:重复步骤3和步骤4,直到找到一个合法的解,或者回溯回到了第一个空白位置,且所有的数字都尝试过。在找到解的过程中,如果遇到了一个不合法的状态,则回溯到上一个空白位置。
4.1.2代码
1. 定义了一个`checkBoardX`函数,用于检查当前数字在数独盘面中是否符合数独规则。该函数会检查当前数字在所在行、所在列、以及所在的3x3小方块内是否存在重复。
2. 定义了一个`helper`函数,用于递归地填充数独盘面中的空格。该函数接受一个参数`index`,表示当前要填充的空格的索引。函数首先判断当前空格是否已经填充数字,如果是空格则尝试填充数字1-9,并使用`checkBoardX`函数进行验证。如果验证通过,则递归调用`helper`函数来填充下一个空格。如果验证不通过,则回溯到上一个空格,并尝试下一个数字。
3. 定义了一个空列表`res`,用于存储所有的解。调用`helper`函数开始求解数独问题。
4. 定义了一个数独盘面`board`,其中0表示空格。
总体来说,对于数独的的求解用回溯算法实现,通过检查每个数字在数独盘面中是否符合数独规则,递归地填充空格,并找到所有解。
4.1.3查看答案按钮的实现
当点击"查看答案"按钮时,触发了`solve_sudoku`方法的调用。该方法会调用`update_output_widgets`方法来更新界面显示解答的部分。
在`update_output_widgets`方法中,首先检查是否存在解答(`self.solution`)。如果存在解答,就会遍历解答列表,然后遍历每个单元格,在相应的输入框中插入解答值。这是通过使用`input_frame.grid_slaves`方法获取对应位置的输入框引用,然后使用`widget.insert`方法来插入解答值实现的。
如果没有找到解答,就会在界面上显示"未找到解答"的标签。
总之,当点击"查看答案"按钮时,该按钮的点击事件会触发更新界面的操作,将解答显示在数独面板上。
4.1.4 提示按钮的实现
当点击"提示"按钮时,触发了`step_solution`方法的调用。该方法会逐步显示数独的解答,每次点击按钮都会显示下一个空白单元格的解答。
在`step_solution`方法中,首先检查是否存在解答(`self.solution`)。如果存在解答,就会遍历解答列表,然后遍历每个单元格。在每次循环中,会检查当前单元格是否为空白(通过判断对应的输入框是否为空)。如果当前单元格为空白,则将解答值插入该单元格的输入框中,并立即返回。这样,每次点击"提示"按钮都会显示下一个空白单元格的解答。
如果没有找到解答,就会在界面上显示"未找到解答"的标签。
总之,点击"提示"按钮会逐步显示数独的解答,每次点击都会填充一个空白单元格的解答值。这样,用户可以逐步获取数独的解答过程。
4.1.5 验证按钮的实现
当点击“验证”按钮时,触发了`verify_solution`方法的调用。该方法会根据用户输入的数独答案与正确答案进行比较,并对输入框中的内容进行验证和标记。
在`verify_solution`方法中,首先检查是否存在正确的解答(`self.solution`)。如果存在正确的解答,就会遍历数独中的每个单元格。
对于每个单元格,首先获取对应的输入框对象。然后判断输入框对象的类型,如果是`tk.Entry`类型,则表示该单元格是一个可编辑的输入框。接下来,获取用户在输入框中输入的值。
如果输入的值不是数字,表示用户输入错误,将该单元格的值置为0,并将输入框的字体颜色设为红色。
如果输入的值是数字,将其转换为整数,并与正确答案进行比较。如果输入的值与正确答案不匹配,将该单元格的值更新为用户输入的值,并将输入框的字体颜色设为红色。如果输入的值与正确答案匹配,将输入框的字体颜色设为黑色。
通过这种方式,点击“验证”按钮后,程序会检查用户输入的数独答案是否正确,并在界面上对错误的输入进行标记,使用户能够立即看到错误的位置。这样用户可以及时纠正错误,并继续进行数独的求解。
- 回溯算法运行演示
当点击"算法运行展示"按钮后,程序会调用`run_algorithm`函数。
在`run_algorithm`函数中,首先会更新GUI界面,使得界面上显示当前的数独盘面。然后通过调用`callback`函数来执行数独求解算法,并传入当前盘面`self.current_solution`以及当前的行号`ro`和列号`co`作为参数。
数独求解算法`数独.sudoku`会在每次递归调用时检查当前数字在盘面中是否成立。具体而言,它会检查当前位置所在行、列以及所在的3x3区域中是否已经存在相同的数字。如果存在相同的数字,则当前数字不符合数独规则,算法会进行回溯。
算法使用递归的方式,从第一个空格开始,依次尝试填入数字1到9,并检查数独规则。如果当前数字符合规则,则继续递归调用函数来填写下一个空格。如果当前数字不符合规则,则回溯到上一个空格,尝试填入下一个数字。
在每次递归调用前,算法会检查是否有可调用的`callback`函数。如果有,则将当前的盘面、行号和列号作为参数传入`callback`函数中。这样可以实现在每次填写一个数字后,更新GUI界面,使得用户能够看到算法的执行过程。
算法会一直执行直到找到数独的解答或者遍历完所有可能的数字组合。如果找到解答,就会更新GUI界面,显示解答结果。如果遍历完所有可能的数字组合但没有找到解答,会显示未找到解答的提示信息。
整个算法运行的过程是通过不断的递归调用实现的,每次调用都会更新GUI界面,使得用户能够观察到算法的执行过程。
经过我的观察,我发现基本一个数独题目要递归调用2w次,我利用回调函数将运行结果在tk界面上完全展示出来还是会导致程序的崩溃,所以我将整个进程2s的时间刷新,保证界面在实现运行演示的同时,能够程序的正常运行。