文章目录
一、项目地址
github地址:https://github.com/ZJT1024/Sudoku
二、各模块开发时间预估
注:实际耗时在结尾处给出。
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) |
---|---|---|
Planning | 计划 | 15 |
Estimatie | 估计这个任务需要多少时间 | 20 |
Development | 开发 | 240 |
Analysis | 需求分析(包括学习新技术) | 30 |
Design Spec | 生成设计文档 | 60 |
Design Review | 实际复审(和同事审核设计文档) | 120 |
Coding Standard | 代码规范(为目前的开发制定合适的规范) | 60 |
Design | 具体设计 | 90 |
Coding | 具体编码 | 360 |
Code Review | 代码复审 | 90 |
Test | 测试(自我测试,修改代码,提交修改) | 300 |
Reporting | 报告 | 90 |
Test Report | 测试报告 | 20 |
Size Measurement | 计算工作量 | 60 |
Postmortem & Process Improvement Plan | 事后总结,并提出过程修改计划 | 30 |
合计 | 1585 |
三、学习过程、解题思路
3.1 开发语言及运行环境
考虑到不同语言的程序运行速度问题,根据题目要求及个人对所要求语言的熟悉程度,本次项目采用C++进行开发,运行环境为64bit Windows10。
3.2 项目要求分析
该项目的主要目的是实现一个能够生成数独终局并能求解数独的控制台程序,此外项目还需包括代码分析、性能测试。由于本次项目需要按照软件工程开发的一般流程进行,所以,除了核心代码之外,代码分析和性能测试就尤为重要。由于该项目具有一定特殊性,即项目需求明确且固定,为方便之后进行单元测试,项目选用增量模型进行开发,每个需求之间采用瀑布模型,设计方法采用结构化的设计方法,尽量做到函数模块之间高内聚低耦合。
3.2.1 需求建模
由题意可知,程序需要能够判断用户输入命令,对于不同命令执行不同子程序,并给出反馈,其中,子程序包括生成数独终局模块和求解数独残局模块。
-
数据建模——ER图描述
通过对题目进行分析,进筛选得到如下实体和实体属性:
指令:输入文件名*、操作指令*、参数*
文件:文件名*、输入输出类型*
数独局:行数据、列数据、宫数据
-
功能建模——数据流图(DFD)
-
- 顶层图(第0层图):
项目主要功能是根据用户提供的合法指令完成数独终局的生成或数独残局的求解,将结果写入指定文件并能在程序出现异常时给用户相应的反馈信息。
- 顶层图(第0层图):
-
- 一层图:
整个项目采用结构化设计,将主要过程进行模块化封装,做到高内聚低耦合。通过对题目的分析,本次项目开发将大致分为四部分,分别为:指令校验模块、生成数独终局(组)模块、残局校验模块、求解数独残局(组)模块。其中,生成数独终局(组)对应数独终局生成功能,残局校验模块和求解数独残局模块对应残局求解功能。
- 一层图:
-
-
二层图:
(指令校验模块)
在指令校验模块中,将指令拆分为三部分分别进行校验,并在校验结束后提取出合法操作符和参数,如果不合法则对用户进行提示。(生成数独终局(组))
在生成数独终局(组)模块中,程序根据终局需求数生成终局,每生成一个新终局就输出一个并计数,节约内存。(残局校验模块)
在残局校验模块,程序根据从文件中独入的数独残局的数字进行校验,检查是否有重复数字,若没有则对完整性进行校验,检查残局是否满足9行9列,若都满足则输出合法残局,否则则想用户输出非法数独残局的反馈信息。(求解数独残局(组))
在求解数独残局(组)模块,程序先统计合法残局中的空位及它周围的数据,之后在对每个空位进行求解,因为合法的残局一定有一个解,所以程序一定能找到一个完整的数独解。
-
-
行为建模——状态转换图
下图展示了数独终局生成和数独残局求解程序的运行过程。
3.2.2 数据流设计方法
- 复审并精华数据流图
进过对数据流图的进一步分析,在“生成数独终局(组)”模块和“求解数独残局(组)”模块之后各增加一个输出模块,将原来的按字符输出转化为按块输出,提高输出效率。得到的数据流简化图如下(其中,模块5与模块6为增加的输出部分):
- 划分自动化边界,确定数据流的特征为变换流
自动化边界的划分如上图虚线所示,数据流图中没有明显的事物处理中心,将其视为变换流。 - 划分数据输入、输出边界,分离出处理部分
输入输出边界的划分如上图大括号所示,其中输入部分包括指令输入与校验和数独残局的输入与校验,变换部分包括数独终局生成和数独残局的求解,输出部分为将对应的数独终局输出到指定文件中。 - 执行“一级分解”
系统的一级分解图表现了系统高层的组织结构和高层模块之间的数据流向,其一级分解图如下图所示:
- 执行“二级分解”
二级分解细化了一级分解的结构组织,下图为系统的二级分解图:
3.3 解题思路
整个项目大致分为四个模块,根据题意,程序在指令模块需要能够判断输入指令是否合法,若合法再进行相应操作;生成数独终局(组)模块程序需要在竟可能短的时间内生成最多不超过1000000个不重复的数独终局;残局校验模块要能够对用户输入的残局进行校验,当残局合法时才进行残局求解计算;求解数独残局(组)模块需要在竟可能短的时间内对最多1000000个合法残局进行求解。
3.3.1 指令校验模块
由题意可知,合法指令有如下两种格式:
sudoku.exe -c 20 // 执行sudoku.exe程序 输入指令-c 20
sudoku.exe -s absolute_path_of_puzzlefile // 执行sudoku.exe程序 输入指令-s absolute_path_of_puzzlefile
所以指令校验模块的任务应该是检验操作符是否为“-c”或“-s”,操作符后的参数是否符合要求,所以该模块只需对指令两个部分分别检验即可。因为除了检验指令合法性,该模块还需要对合法指令进行操作符合参数的提取,所以不放接口参数直接用引用类型,将参数直接赋值,而操作符用整型(1/0)或bool型返回。
3.3.2 生成数独终局(组)模块
-
暴力枚举
对于少量的数独终局,我最先想到的是暴力枚举,即每个位随机取[0,9]的整型数字,之后判断是否合法,直到填满81个空格,在生成一个数独总局之后进行查重…显然,这是一种非常费时的算法,就算进行适当优化,在不考虑输出的情况下,假设每个位置的数字均能一次随机出合法数字,则每个数字的合法性的检验需要O(n)的复杂度,生成一个数独终局需要O(n)的复杂度。之后是查重,对n个二维数组查重需要O(n^3)的复杂度。
所以,在特别理想的情况下,该算法的时间复杂度为O(n^3),显然不能满足1000000数量的求解问题… -
全排列算法
通过简单观察以及查阅资料得知,全排列是生成数独终局的有效算法之一。以第一行为下列数据为例:1 2 3 4 5 6 7 8 9
当第二行向左(或右)平移(n % 9)位,且(n % 9 != 0)时,该两行中的任意两列元素一定不同。同理,之后的7行也做类似处理,这样就能初步保证9行中任意两列没有重复的元素,因为每一行是由平移得到的,所以只要保证了第一行没有重复元素,之后的9行中任意一行都不会有重复元素。之后就是保证每个3X3的宫内元素不重复。
通过尝试发现,当从第二行开始,每行依次向一个方向平移3、6、1、4、7、2、5、8个单位时,每个宫内的元素不重复。至此一个有效的数独终局便已形成。该算法的优势在于,由于平移的原理,在首行无重复元素的情况下,生成的数独终局一定合法,不需要再对每个元素进行合法性检验,时间复杂度为O(1);其次,只要首行不同,生成的终局一定不同(至少首行不同),所以只要保证首行不同,就不用进行查重,时间复杂度为O(n)。
现在问题就有求解一个数独终局转化成了求解8个数(第一位固定为8)的全排列问题。由于8个数规模不大,只需使用简单的递归便能实现8个数的全排列,复杂度为O