软件工程第三次作业---数独
一.GitHub地址---->点击穿越
二.PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 60 | 30 |
Estimate | 估计这个任务需要多少时间 | 2100 | 2210 |
Development | 开发 | 2100 | 2210 |
Analysis | 需求分析(包括学新技术) | 480 | 400 |
Design Spec | 生成设计文档 | 30 | 60 |
Design Review | 设计复审 | 30 | 20 |
Coding Standard | 代码规范(为目前的开发指定合适的规范) | 30 | 40 |
Design | 具体设计 | 60 | 90 |
Coding | 具体编码 | 900 | 930 |
Code Review | 代码复审 | 30 | 15 |
Test | 测试(自我测试,修改代码,提交修改) | 120 | 180 |
Reporting | 报告 | 240 | 120 |
Test Repor | 测试报告 | 120 | 120 |
Size Measurement | 计算工作量 | 30 | 15 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 240 | 240 |
合计 | 2100 | 2210 |
三.解题思路
拿到这道题,首先想到的是我长到这么大竟然没有做过一张数独?(小羞耻。。)那么我首先要做的一步就是了解数独规则。
1.了解规则
百度百科简介:
数独盘面是个九宫,每一宫又分为九个小格。在这八十一格中给出一定的已知数字和解题条件,利用逻辑和推理,在其他的空格上填入1-9的数字。使1-9每个数字在每一行、每一列和每一宫中都只出现一次,所以又称“九宫格”。
2.思路
考虑到如果从三宫格开始的话,代码中缺少不对宫的判断,不利于后续阶数的扩展,我想直接从常规九宫格的解题开始。
首先开始阶段.
要先初始化数独盘,在题目已经填写的数字基础上对我们要填的格子进行填充限制,如下图所示:
然后是数字填充阶段.
思路比较常规,回溯法,就是在已有的限制条件下,按顺序尝试1-9在格子中的填充,如果在尝试过程中填写到了数独盘的最后一格,则代表数独已经解出来了(此方法仅限提供的数独盘只有唯一解,如果有多解也只能输出一个),如果到达某格无法继续填充后续数字,需要移除之前放置的数字,然后继续尝试,如下图所示:
图是用画图工具画的,很粗糙……
四.代码组织
五.关键代码
回溯方法:
/**
* 回溯填充方法
* @param row
* @param col
*/
public void backtrack(int row, int col) {
if(shudoPan[row][col] == 0) {
for(int d = 1; d <= m; d++) {
int idx = 0;
if(boxRow > 0) {
idx = (row / boxRow ) * boxRow + col / boxCol;
}
if(couldPlace(d, row, col)) {
//填充数字,并设置填充限制
boxOccupied[idx][d]++;
rowOccupied[row][d]++;
colOccupied[col][d]++;
shudoPan[row][col] = d;
//是否填充到最后一格
if ((col == m-1) && (row == m-1)) {
sudokuSolved = true;
}
else {
//当到达最后一列的格子,下一个格子跳转到下一行
if (col == m-1) {
backtrack(row + 1, 0);
}else {
backtrack(row, col + 1);
}
}
if(!sudokuSolved) {//移除填充后无法进行后续填充的数
boxOccupied[idx][d]--;
rowOccupied[row][d]--;
colOccupied[col][d]--;
shudoPan[row][col] = 0;
}
}
}
}else {
if ((col == m-1) && (row == m-1)) {
sudokuSolved = true;
}
else {
//当到达最后一列的格子,下一个格子跳转到下一行
if (col == m-1) {
backtrack(row + 1, 0);
}else {
backtrack(row, col + 1);
}
}
}
}
解数独方法---用于初始化和调用回溯方法。
代码中的idx = (i / boxRow ) * boxRow + k / boxCol;是根据m宫格的宫格行列大小boxROW、boxCol来确定待解格子所在宫号(假设宫按顺序从左到右,自上而下标号为0~(idx-1))。
/**
* 解数独方法
*/
public void solveSudoku(int[][] shudoPan) {
setBox();//调用设置宫的行列数方法
//System.out.println("boxRow,boxCol:"+boxRow+" "+boxCol);
// 初始化某数所在行、列、宫
for (int i = 0; i < m; i++) {
for (int k = 0; k < m; k++) {
int num = shudoPan[i][k];
if (num != 0) {
int d = num;
if(boxRow > 0) {
int idx = (i / boxRow ) * boxRow + k / boxCol;
boxOccupied[idx][d]++;
}
rowOccupied[i][d]++;
colOccupied[k][d]++;
}
}
}
backtrack(0, 0);
}
}
设定宫的大小.
如果宫格阶数为3、5、7的话就把代表宫格行列大小的boxRow、boxCol设为-1,用于后面判断使用。这样的好处是实现了对原来9宫格功能的扩展。
/**
* 设定宫的大小
*/
public static void setBox() {
if(m == 4) {
boxRow = 2;
boxCol = 2;
}
if(m == 6) {
boxRow = 2;
boxCol = 3;
}
if(m == 8) {
boxRow = 4;
boxCol = 2;
}
if(m == 9) {
boxRow = 3;
boxCol = 3;
}
if(m == 3 || m == 5 || m == 7) {
boxRow = -1;
}
}
对文件中读取到的数据,取得其中第numb个数独盘并进行解数独。
其中包含三行
Initialize(rowOccupied);
Initialize(colOccupied);
Initialize(boxOccupied);
是个循环初始化占位数组方法Initialize,用于计算完一个数独盘后将用来标记占位的三个数组重新归0;
刚开始我的程序死活只能算完第一个数独,后面的N-1个都原样输出。头疼了一会儿才发现,在第一个数独盘算完后,标记数组还保存着上个数独盘的信息,于是加了这个后就能正常解后续数独了。(这个代码就是个普通的循环赋值的方法,所以就不放出来了)
/**
* 取得第numb个数独盘并进行解数独
* @param numb
* @param m
*/
public void getAndDO(int numb) {
int index;
for(int i = 0; i < m; i++) {
index = numb*m+i;
Slipt(hang.get(index));
}
//将三个判断占位的数组初始化为0,把判断数独是否解完初始化为false
Initialize(rowOccupied);
Initialize(colOccupied);
Initialize(boxOccupied);
sudokuSolved = false;
solveSudoku(shudoPan);
j = 0;
}
六.测试
七.异常处理
1.参数传入后,对参数进行处理阶段,可能因为输入的格式问题导致程序崩溃,所以要在命令行传参方法中添加if,else判断语句,确保程序不崩溃,并报错。
2.参数传入后在,对所要读取文件进行查找,可能因为找不到目标文件而导致程序崩溃,所以添加判断所读文件是否存在的判断方法,并报错。
3.读取文件阶段,读取方法可能因为读取对象创建失败而抛出NullPointerException异常;或着在文件读取过程中遇到问题而抛出IOException异常,要扑捉异常并处理(这两种情况的可能性比较低);也可能因为读取的目标文件为空或者数独盘少于要求的数量n,可能导致数组越界IndexOutOfBoundsException异常,需要报错。
八.代码覆盖率
1.正常运行情况下,
eg. java sudoku -m 3 -n 5 -i input3.txt -o output3.txt
2.缺参运行情况下,
eg. java sudoku -m -n 5 -i input3.txt -o output3.txt
3.文件读取为空情况下,
九.性能测试——JProfiler
1.在请教大佬后,我在主方法开始时加入一个Scanner获取键盘输入,记录下原程序开始前状态(内存和CPU)。
2.然后从键盘随意输入一串字符串后,开始原程序,记录下此时状态(内存和CPU)。(红色部分为原程序运行后情况)
显示消耗时间最多的方法的列表。
十.总结
这次作业不算太难,但由于我还是Java初学者,有些知识我还没学到,比如读取文件、写文件、异常处理等,第一步的传参我也瞎搞了好久,刚开始想直接把args[1]、args[3]、args[5]、args[7]直接传入就好了,后来看了助教发的代码,我才意识到这样做是远远不够的。最难的是测试阶段,JUnit、JProfiler等等,还是不怎么会,单元测试还是写不好,要再花时间在这方面上。
写代码花的时间也比较长,因为一边学一边写,断断续续的。