软件工程第三次作业---数独

软件工程第三次作业---数独


一.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;
	}

六.测试

3、4、5、6、7、8、9阶测试






七.异常处理

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等等,还是不怎么会,单元测试还是写不好,要再花时间在这方面上。
写代码花的时间也比较长,因为一边学一边写,断断续续的。

posted @ 2019-09-25 01:02  白糖黄连混合物  阅读(428)  评论(2编辑  收藏  举报