Suduku——软件工程项目实记
1. 项目描述
Github地址:SudukuProject - Github
通过分析,本次软工项目需要实现两个与数独有关的主要任务。
1.生成多个(1-106个)无重复的数独终局到文件。
2.求解多个(1-106个)数独残局,并将结果输出至文件。
2. PSP 2.1 表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 20 |
· Estimate | · 估计这个任务需要多少时间 | 30 | 20 |
Development | 开发 | 1000 | 1440 |
· Analysis | · 需求分析(包括学习新技术) | 200 | 300 |
· Design Spec | · 生成设计文档 | 60 | 90 |
· Design Review | · 设计复审(和同事审核设计文档) | 30 | 30 |
· Coding Standard | · 代码规范(为目前的开发制定合适的规范) | 60 | 60 |
· Design | · 具体设计 | 120 | 180 |
· Coding | · 具体编码 | 300 | 450 |
· Code Review | · 代码复审 | 30 | 30 |
· Test | · 测试(自我测试,修改代码,修改提交) | 200 | 300 |
Reproting | 报告 | 270 | 330 |
·Test Report | · 测试报告 | 180 | 240 |
·Size Measurement | · 计算工作量 | 30 | 30 |
·Postmortem & Process Improvement Plan | · 事后总结,并提出改进计划 | 60 | 60 |
合计 | 1300 | 1700 |
3. 确定解题思路
1)生成数独终局
-
第一种想法是暴力回溯法求解,这个时候分两种填入方法。第一种是从左上到右下,每次将1-9以随机位置填入一行,通过随机化回溯调换数字位置,使符合规则。第二种是按照数独规则,每次填入一种数字,填入后行列宫格均不重复,再开始填入下一个数字。
这种方法思路比较清晰,生成的数独比较多样,因为每个数独只有9*9大小,所以预测回溯用时在经过优化后还是可以接受的,将其加入待用方案。 -
第二种方法是模板法,这种方法是经过查找各种资料找到的。模板法的第一步都是先生成一种合法的数独盘面作为模板,根据后续对模板处理的不同思路,将其分为三种比较有代表性的方法。
- 第一种的处理是以宫格为单位的矩阵转换法,生成合法的中间3*3宫格后,经过小宫格的行变换填充左右相邻宫格,有2!种填充方法,经过小宫格列变换填充上下相邻宫格,有2!种填充方法再经过行列变换结合填充四个角上的宫格,有4!*2种填充。采用这种方法,每种中间宫格对应192种填充方式,中间宫格共有9!种填充方式,计算得这种方法共能生成362880×384种,大于106种,将其加入待选方案。
- 第二种是在生成基本模板终盘后,用a-i替换数字1-9,对模板进行行列变换,1-3、4-6、7-9行每3行或列中单独变换。由于左上第一个数字固定,1-3行有2!种交换结果,4-6行有3!种变换结果,7-9行有3!种变换结果,计算可得共有8!×3!×2!×3!=40320×72种数独终盘,大于106,将其加入待选方案。
- 第三种处理是以行列为单位,对数独行列模板进行平移变换生成新的行或列,为了简化说明,这里只以行变换为例。每一确定的行的数字向右平移后,可得到9个不同的行,把它们按一定顺序排列即可得到符合规则的数独盘面。
令Sn为第n行向右平移的格数,S1可认为等于0,|S2-S1|≥3且|S3-S2|≥3同时成立即可得到合格的前三行,第4-6与第7-8行同理,同时S1到S9互不相等。在此种方法中用a-i代替数字1-9,由于第一行左边第一个数字要求固定,因此第一行有8!种填充方式,平移时,前三行有{0,3,6}{0,6,3}两种平移方式,4-6行可分别以1、4、7以2、5、8进行3!×2种平移,7-9行同理。
此种方法可得8!×3!×2×3!=1451520×2种数独终盘,大于106,将其加入待选方案。
-
经过对问题得分析,以上方案都可以做出尝试,但结合实现的难度,个人选择模板法,又因为后两种模板法有一定相似之处,结合代码实现的复杂度,选择模板较为简单的平移法。
2)数独残局求解
- 求解数独时也准备了两种方法。一种是采用暴力回溯法,用DFS深度优先法进行优化。一种是Dancing Links解法,类比精准覆盖问题求解。由于第二种方法实现比较复杂,准备先用第一种方法实现一遍看性能怎样。
- 在使用回溯算法中,经过学姐提醒,借鉴Forward-checking算法进行优化,加入约束传播步骤加快求解数独的速度与精度。
4. 实现过程设计
- 程序主要实现生成数独及求解数独两种功能,因此将程序分为两大模块。
- 首先,通过读取调用命令判断需要实现的功能,并进入相关模块。本程序考虑了多种可能出现的调用指令,并给出了相应的错误提示,报错能力较强。
1. 在生成数独模块中,主要函数为ConnectChanges,这部分函数实现随机数的生成及对模板的填充,将填充好的数独终盘存入数组,并通过调用RowChanges函数对模板进行行模板的平移变换,最后当主函数判断出所有数独终盘已经生成完毕时,调用CreatOutput函数进行统一的文件输出。
2. 在求解数独中,主函数先调用GetProblem读入81个数字,判断为数独题目后,调用SolveSudoku 函数进行求解,此函数为求解部分的主要函数,也为回溯部分,此函数通过调用 MarkVis函数对出现过的数字进行标记,并通过Check判断填入数字是否合法,若不合法则回溯。当此数独完成后,调用SaveSolution函数进行规范输出。
5. 性能改进与性能分析
- 因本项目对程序运行时间有所限制,因此,本程序在设计和编码阶段都做了大量优化,主要体现在以下方面。
1.经过资料搜索,程序的输入输出部分将占用大量时间,因此,为了减少输出次数,优化输出时间,在生成数独模块中,掘弃了生成一个终盘即输出的做法,而是申请了一个一维大数组,将数字及换行符还有空格存入,虽然在代码实现阶段会复杂一点,但只用打开一次文件,就可以进行整个结果数组的输出。
2.在设计初期,想用更容易实现的暴力搜索,但后来的查资料过程中,接触到了各种算法,通过对这些算法的借鉴和学习,对回溯进行了条件约束,使回溯步数大大减少,优化了程序运行时间。 - 因此,在代码初版完成后,针对性能改进方面基本没有太大的更改。
- 以下是程序生成1000盘数独终盘,所用时间及函数使用情况。总用时4.055秒。(因未知原因函数名发生了改变,但看函数调用逻辑基本不影响阅读。)