数独求解——回溯法
Github地址→
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 10 |
Estimate | 估计这个任务需要多少时间 | 1260 | 1540 |
Development | 开发 | 960 | 1200 |
Analysis | 需求分析 (包括学习新技术) | 180 | 320 |
Design Spec | 生成设计文档 | 60 | 30 |
Design Review | 设计复审 | 120 | 90 |
Coding Standard | 代码规范 (为目前的开发制定合适的规范) | 0 | 0 |
Design | 具体设计 | 120 | 60 |
Coding | 具体编码 | 360 | 360 |
Code Review | 代码复审 | 120 | 120 |
Test | 测试(自我测试,修改代码,提交修改) | 180 | 240 |
Reporting | 报告 | 300 | 320 |
Test Repor | 测试报告 | 60 | 60 |
Size Measurement | 计算工作量 | 60 | 20 |
Postmortem & Process Improvement Plan | 事后总结, 并提出过程改进计划 | 180 | 240 |
合计 | 1260 | 1540 |
题目要求
实现一个命令行程序,不妨称之为Sudoku。
- 具体任务:
现在我们想一步一步来,完成从三宫格到九宫格的进阶;完成三宫格和其他博客任务,就算过了初级考核,其他的算升级。具体各阶规则如下:
三宫格:盘面是33。使1-3每个数字在每一行、每一列中都只出现一次,不考虑宫;
四宫格:盘面是22四个宫,每一宫又分为22四个小格。使1-4每个数字在每一行、每一列和每一宫中都只出现一次;
五宫格:盘面是55。使1-5每个数字在每一行、每一列中都只出现一次,不考虑宫;
六宫格:盘面是23六个宫,每一宫又分为32六个小格。使1-6每个数字在每一行、每一列和每一宫中都只出现一次;
七宫格:盘面是77。使1-7每个数字在每一行、每一列中都只出现一次,不考虑宫;
八宫格:盘面是42八个宫,每一宫又分为24八个小格。使1-8每个数字在每一行、每一列和每一宫中都只出现一次;
九宫格:盘面是33九个宫,每一宫又分为3*3九个小格。使1-9每个数字在每一行、每一列和每一宫中都只出现一次;
解题思路
一.要解决的要点
1.实现文件的读入并存入数组及文件的写出
2.设计数独函数:包括要选择什么算法、用几个子函数实现、是否要建立Class实现某些功能、函数的返回应该如何设计。
3.代码复审:某些函数是否可以使用更简洁方法解决;是否要再进行某些函数的封装使代码看上去更清晰,提高可读性;可以在代码内哪个地方测试输出,让数独填写过程清晰明了,更利于找到出错的地方。
二.函数流程图
三.我的解题过程
第一点:实现文件的读入并存入数组及文件的写出
先建立出一个可以读入输出的框架,这个框架要可以从文件中读取字符串填充二维数组,同时可以将二维数组写入输出文件- 我的初试
在这点上我纠结了很久,因为暑假才接触java,很多函数的使用都不熟练,前前后后查找学习api文档花了1小时左右的时间,同时在CDNS上看如何把读到的字符串写入数组也花了2~3小时左右,因为一直设想有没有可以直接读入数组的方法,而不是用readline和split。
后面在函数的分装上也花了很多时间,一开始是想用3个函数:①读取文件并写入数组,②解数独函数,③输出函数完成一个盘面的解答,但是要是分装读入并写入数组为函数就要在那个函数中使用BufferedReader,而读入文件的BufferedReader定义在主函数里,在子函数里面再建一个管道我觉得没有必要,也尝试过把BufferedReader作为参数传给子程序,结果是读取乱序。 - 最后方案
使用BufferedReader的readline读入文件,再用split分装数组。System.setOut将输出指向输出文件,在主函数里面实现一个总的输入循环并把解数独的函数嵌套其中,把输出放在解数独的函数中实现。
- 我的初试
BufferedReader br =
new BufferedReader(new FileReader(inputFileName));
PrintStream ps = new PrintStream(new FileOutputStream(outputFileName,true));
System.setOut(ps);
第二点:设计数独函数
- 我的初想法
把每一列、每一行、每一宫都转化为一维数组,然后检索一行、一列或者一宫里面只有一个空格要填确定的空填上,在相应的行列宫的相应位置填上空。对无法唯一确定的空设置一个可能值数组,里面是除去行、列、宫已有数字外的可能。 - 最终的实现
采用回溯法,用循环直接填上没有出现在行、列、宫中的数字,如果出错后面会回溯回来更改。
没有建立额外的class,采用行、列、宫分别遍历确定所填数字。这里判断宫内是否含有传送数字会相对复杂,因为4、6、8、9的宫都不一样,在宫开始遍历前要划分遍历的宫的行数和列数。 - 更改的原因
原来的想法有几个难点:①.建立行、列、宫三种一维数组与一个空二维数组位的联系。②.从最小空填起的方式要求每次都要遍历一遍数组,会造成填空的无序,时间花费大;建立空格的可能数组也会造成大量的时间空间开销。 - 代码展示
- 我的初想法
static void sudokuSolve(int row , int col)/*解数独主函数*/
{
/*System.out.println(row+" "+col);//代码测试
writeOutArray();*/
if(row>phraseWordNum||col>phraseWordNum||row<0||col<0)
{
System.out.println("待填空缺获取坐标越界");
System.exit(-1);
}
for(int i = 1 ; i <= phraseWordNum ;i++)
{
if(differentInRow(row,i)&&differentInCol(col,i)&&differentInPalace(row,col,i))/*填上行列宫都没有出现过的数字*/
{
sudoku[row][col] = i ;
emptyNum--;
if(emptyNum==0)
{
writeOutArray();
break;
}
int tempArray[] ;
tempArray = nextEmpty(row,col);
sudokuSolve(tempArray[0] ,tempArray[1]);
sudoku[row][col] = 0;/*回溯*/
emptyNum++;
}
}
}
判断行、列的代码比较简单就不列出,这里只显示判断宫内有无传送数字的代码
static boolean differentInPalace(int row, int col,int tempNum)//同宫无与temp相同的数字返回true
{
int rowNum,colNum;
if(phraseWordNum==3||phraseWordNum==5||phraseWordNum==7)//划分遍历的宫的行数和列数
return true;
else if(phraseWordNum==4||phraseWordNum==6)
{
rowNum = 2 ;
colNum = phraseWordNum/2;
}
else if(phraseWordNum==8)
{
rowNum = 4 ;
colNum = 2;
}
else
rowNum = colNum = 3;
int rowStart=row/rowNum*rowNum , colStart = col/colNum*colNum;
for(int i = rowStart; i
第三点:代码复审
最后简化了几个函数,添加了一些异常处理,在sudokuSolve函数内开头设置输出空格坐标和打印当前盘面,结果就可以很清晰的看出哪里有错然后针对更改。
- 测试代码段
static void sudokuSolve(int row , int col)/*解数独主函数*/
{
/*System.out.println(row+" "+col);//代码测试
writeOutArray();*/
样例测试
这里采用助教的样例测试,助教txt中两个相邻数字间隔两个空格,但是博客作业中指出是一个空格,所以测试样例时代码相对提交的代码有些许改动,并且因为结果截图过长,只截取测试的前两个盘面的结果
性能测试
在配置上真的卡了很久(虽然最后感觉不是配置问题?),也看过不下40篇CSDN教程了,最后还是找大佬请教才做出来的,之后有时间可能会写一个jprofiler性能测试博客记录一下,让同样有困扰的人能够参考
之前一直卡住的问题界面也在这边放一下
单元测试
还在摸索中
还未解决的问题
我在editplus上打完代码后用idea打开了代码的java文件,结果之后它在运行的时候就会对中文报错,而且不是全部报错,是部分报错,用了如图的encoding修改编码也没用,修改idea的编码也没用(;´༎ຶД༎ຶ`)
体会总结
我认为这次的作业于我而言比起打代码的过程,我花费更多在学习的过程上,在几天百度搜索上面的框都是满到要按后移键的那种,从一开始输入的流的选择,选择DataInputStream还是BufferedStream,从各种函数内部参数类型应该是String还是int,这里应该初始化,那里应该有异常处理,这个括号少了,哪个分号少了。这几天看到的各种bug真的是学java以来见过最多的,几天前打出功能完整的代码时,看到200行的代码我甚至不敢运行,因为怕看到满屏的错误。
当然学习收获也是非常多,idea的学习,java的学习,Github的学习,甚至是现在写这个博客MarkDown各种排版的学习。再回头看那某几行简短但是投入了一两个小时的尝试才得出最终结论得代码时,真的是鼓起了对未来的学习更多尝试的勇气和力量。