解数独——命令行程序的实现
Github项目地址
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(min) | 实际耗时(min) |
---|---|---|---|
Planning | 计划 | 30 | 15 |
Estimate | 估计这个任务需要多少时间 | 48*60 | 12*60 |
Development | 开发 | 20*60 | 10*60 |
Analysis | 需求分析 (包括学习新技术) | 2*60 | 5*60 |
Design Spec | 生成设计文档 | 20 | 15 |
Design Review | 设计复审 | 10 | 10 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 30 | 60 |
Design | 具体设计 | 2*60 | 60 |
Coding | 具体编码 | 12*60 | 5*60 |
Code Review | 代码复审 | 20 | 30 |
Test | 测试(自我测试,修改代码,提交修改) | 60 | 60 |
Reporting | 报告 | 60 | 2*60 |
Test Repor | 测试报告 | 30 | 30 |
Size Measurement | 计算工作量 | 15 | 10 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 15 | 10 |
Sum | 合计 | 5320 | 2340 |
思路描述
一开始拿到这道题,其实我是拒绝的,一直比较抗拒这种矩阵处理的题,看测试数据和题目描述就已经让我想放上几天〒▽〒。后来啊,DeadLine到了~~~
不管了,硬着头皮也要上!首先是仔细地读了一遍题,原来是需要实现一个能解数独的程序!之前的畏难心理已经消退了不少;静下心后打开搜索引擎,搜索数独相关的资料,了解到数独是需要我们去按规则填写数字:同一行同一列不能出现相同数字,非素数阶数还需要满足每个宫内不能有相同数字。
第一种想法 —— 蛮力法
既然是定好范围的数字中选数填空,那肯定可以使用蛮力法:
生成所有可能用范围内数字填充格子为0的解,检查是否合法并保留解,但这样的话,每个格子都需要跑一遍,这将是一个指数级算法,不能采用。
第二种想法 —— DFS
使用搜索来确定每个格子可以填充的数字,嵌套递归能使代码量降低,但是自己只懂一点皮毛,目前花大量时间去练DFS有点来不及啦,遂作罢。
第三种想法 —— 回溯法
这个想法不是自己想到的,在阅读大量关于求解数独的博客以及一个题解视频后,明白了其中的奥秘,代码也不难,就决定是你了!
设计实现过程
本次程序设计采用定义与实现分离的写法,所有操作由stdafx.cpp完成,声明在stdafx.h中,参数接收由Sudoku.cpp完成,最终由编译器链接成为Sudoku.exe:
- 类(1个):solveSudoku
- 函数(4个):init、helper、isValid为私有函数,solve为公有函数
其中solve函数整合了输入输出操作、init初始化盘面函数以及helper解题函数,在helper中使用isValid函数进行数字合法与否的判断。流程图如下:
单元测试
《构建之法》第二章提到了单元测试对于程序的重要性,这也是我目前学习的目标,截止到现在还未去进行实际操作,只了解到VS可以新建一个单元测试项目,导入需要测试的函数,若完成了测试会显示绿勾(还是不太明白其中的原理,难道是想PTA那样对比输入输出是否满足预期?),之后补上。目前只会通过运行整个程序并通过输入输出是否达到预期的这种方式来测试,如下图所示,最后三张图片为异常:
三阶
四阶
五阶
六阶
七阶
八阶
九阶
异常
无解情况
argv参数设置错误
后来发现argv[0]是路径....学到了
性能分析
先上图,这是整体概览:
------------------------------------------ 分割线 ------------------------------------------
在《构建之法》第二章中根据函数调用次数及所耗时间比,优化了一个对象调用其大小的for循环条件,优化了程序性能。通过这张图可以发现满足优化条件的函数分别为helper和isValid,虽然vector的[]运算符重载函数被调用了7万次,但对我而言是无法去做优化的,只能平静的接受了~
helper优化的思路是加上DFS并剪枝(目前只在说说阶段,还不知道怎么去实现),isValid优化的话——目前只想到将算术运算转为二进制位运算来提升速度。
关键代码及解析
要我选出这次程序设计的关键代码,我会选择solve函数:
/* @param board 盘面 @param size 盘面大小 该函数为数独的回溯函数,通过反复调用和回溯达到解决问题的目的。 */ bool solveSudoku::helper(vector<vector<char>>& board, int size) { for (int i = 0; i < size; i++) // 盘面遍历 { for (int j = 0; j < size; j++) { if (board[i][j] == '0') // 填数条件 { char max_num = size + '0'; // 选数 for (char num = '1'; num <= max_num; num++) { // 合法性判断 if (isValid(board, size, i, j, num)) { board[i][j] = num; // 进入下一层 if (helper(board, size)) return true; // 不回溯 board[i][j] = '0'; // 回溯后的还原 } } return false; // 回溯 } } } return true; // 遍历完成结束标志 }
首先我们从头到尾遍历盘面,如果当前位置是0,我们就进行填数操作,并判断合法性。紧接着若数字合法,我们就将数填入盘面中,并进入到下一层,这是回溯法的关键:如果下一层判断不合法,将回退到上一层并还原为0。在这一次次的推进与回溯中,程序将完成数独的填写操作。
心路历程与收获
- 从畏难心理到坦然面对
- 遇到问题应该静下心来多读几遍题面和分析,而不是直接放掉
- 了解到《构建之法》中关于如何完整构建一个软件的思路和过程
- 明白了单元测试的重要性,并将付诸实现
- 了解到更加深层次的程序性能分析和优化,不能去盲目优化
- 更清晰的了解到自己目前的水平,还需努力呀!