第二次作业——个人项目实战

1、Github项目地址

2、PSP表格(预估)

PSP2.1 Personal Software Process Stages 预估耗时(分钟)
Planning 计划 10
· Estimate · 估计这个任务需要多少时间 10
Development 开发 330
· Analysis · 需求分析 (包括学习新技术) 60
· Design Spec · 生成设计文档 60
· Design Review · 设计复审 (和同事审核设计文档) 20
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 10
· Design · 具体设计 60
· Coding · 具体编码 60
· Code Review · 代码复审 30
· Test · 测试(自我测试,修改代码,提交修改) 30
Reporting 报告 60
· Test Report · 测试报告 20
· Size Measurement · 计算工作量 10
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 30
合计 400

3、解题思路

  数独之前虽然没玩过,不过也了解过,知道规则。看到题目的N个不重复第一时间想到的就是可以利用1-9的全排列来增加数量。在得到一个棋盘后对全局的任意数字进行交换,是不影响棋盘的解答性的,因为9!=362 880,所以利用9个数字的全排列可以使得棋盘个数增加5个数量级。
  然后是考虑得到已解答的数独棋盘。在决定用全排列增加数量后,那也就可以把第一行固定,比如直接就是顺序1-9,然后在考虑底下就好了。由于上面使用了全排列,所以自然而然地想到可以对每一行进行全排列,然后看看是否可行,可以列出所有情况。不过随即就考虑到太繁琐了,就像到了最后一层就只有一种可能,但是照排列算的话还是要试9!种。然后就是一格一格试,也就是用深搜回溯。这样的话结合全排列可以得到全部的棋盘。
  之后搜索了一下数独棋盘的生成方法,发现还有一种变换的思想,就是在已生成的棋盘进行局部调换或者随机生成一个宫,再进行行列变换生成其它宫的方法。不过这种方法要保证不重复的话需要存储已经生成过得棋盘,所以也就不考虑这种思想。
  既然生成棋盘的大致方式确定了,那剩下就是随机性的问题了。全排列加上回溯法都是看上去会比较有规律的,我首先考虑就是回溯法的每个位置的数字选用顺序要随机。然后如果是全排列的话,看上去棋盘的规律性会非常明显,所以我考虑取消全排列,用随机的方式替代,因此我就查了一下数独棋盘的个数极限,根据论文所说在21个数量,即便去除全排列的5个数量级,还是有16个数量级,因此我觉得用随机替代全排列结果数量也应该足够大了。

4、设计实现过程

  文件分为主调用文件和生成器实现文件。主调用文件负责处理输入参数,确定输出源。生成器生成文件则负责棋盘的生成和输出。生成器中由一个类构成,分别需要包括初始化,生成下一个棋盘,和输出当前棋盘三个函数。其中生成下一个棋盘中又分成两部分,一部分是将代表数字随机化,另一部分则是在固定首行序列下生成下一个棋盘表。数字随机化是遍历每个元素,然后与随机一个元素进行交换。因为采用的是整型,所以使用异或操作,既省时间也省空间。在第二部分中,如果当前已经是一个有效棋盘,则去除最后一位,然后添加候选的下一个数字,如果没有,则回到上一位,直到有候选。然后如果当前棋盘未满,则继续生成下一位的候选数字队列。在每次生成候选队列或者去除一位时,需要更新该位置的值以及该数字对棋盘当前的影响,而生成下一位的候选数字队列时,也需要各数字对棋盘当前的影响。为了节省空间和时间,我决定采用位运算来实现。用一个长度为9的整型数组,每个值都代表一个数对棋盘的影响,用低9位代表九宫是否放置,用稍高9位代表行是否放置,用更高九位代表列是否方式。更新状态的话就是对相应的位使用异或操作。
  生成棋盘流程图大致如下:

5、代码说明

bool SudokuGenerator::nextTable() {

	bool isActive = true;

	//当前有效棋盘,去除最后一位后清理
	if (candidateStack.size() == FULL_STACK_SIZE) {
		updateTail();
		candidateStack.top().pop();
		isActive = cleanAndActive();
	}

	//添加至满位
	while (isActive && candidateStack.size() < FULL_STACK_SIZE) {
		nextCandidate();
		isActive = cleanAndActive();
	}

	return isActive;
}

  该函数作用是生成下一个棋盘序列,在当前是有效棋盘的情况下,去除最后一位,然后清理空的部分并回退,并生效最后一位的有效数字。然后不断添加下一位的候选列表,直至满位。

void SudokuGenerator::nextCandidate() {
	int i, seq;
	//补上第一行
	int row = candidateStack.size() / SUDOKU_SIZE + 1;
	int col = candidateStack.size() % SUDOKU_SIZE;

	candidateStack.push(std::stack<int>());

	for (i = 0; i < SUDOKU_SIZE; i++) {
		seq = sequenceBase[i] - 1;
		//判断i在九宫,行和列中无重复
		if (!(mask[seq] & (BOX_OFFSET << getBox(row, col))
			|| mask[seq] & (ROW_OFFSET << row)
			|| mask[seq] & (COL_OFFSET << col)
			)) {
			candidateStack.top().push(seq);
		}
	}
}

  该函数是添加下一位的候选列表。首先获取坐标信息,然后按照sequenceBase(序列所代表实际数字)的值来确定候选顺序,然后使用掩码判断每一个数字在当前位置的可放置性决定是否放入候选列表。

bool SudokuGenerator::cleanAndActive() {
	bool isActive = true;
	//当前有空候选必须回退上一位的最后候选
	while (isActive && !candidateStack.top().size()) {
		candidateStack.pop();
		isActive = updateTail();
		if (isActive) {
			candidateStack.top().pop();
		}
	}
	isActive = updateTail();
	return isActive;
}

  该函数的作用是清理尾部空的候选列表,在清理掉空候选列表的时候,必须同时除去上一位的最后候选。最后要使有效的最后一位状态更新。

6、测试运行

7、性能分析


  当n=10000时性能分析如上图。在图中圈出了三个占用最大的函数,其中,输出函数在意料之中占了大比,同时在另外两个部分中详细查看其中是可以发现其中的stl的栈的操作占了大比,因为有较为频繁的入栈出栈操作。如果想进行优化的话,可以抛弃c++的便捷性,用数组模拟栈,在出栈的时候仅修改大小不处理具体元素,应该会快上不少。同时输出也可以用c的标准输出方式替换c++的流输出方式。其他程序流程上的优化暂时没有思路。

附:
  原本是没有什么优化思路的,不过看了老师的评论,觉得在输出方面应该还是有优化的余地的,而不是仅仅把c++的输出换成c的形式。
  其实本来也有考虑过做一个输出缓冲区,因为输出的时候是在循环中不断地输出字符,频繁的与低速的IO进行交互,所以为输出内容设立一个缓冲区,应该能提高速度。而且设立缓冲区后,空格和换行也只要在初始化的时候赋值一次就够了。不过当时自己考虑了一下,觉得提升应该不会很大。不过输出的优化方向暂时也只想到这个了。在实际实现后的性能分析图如下:

  图中可以看出,输出占比大幅度下降,时间降低超过一半,大大出乎我的意料。我以为一万的数据量也就能降个一两秒,没想到差别这么大,还是太年轻。IO的瓶颈还是很巨大的。

8、PSP表格(实际)

PSP2.1 Personal Software Process Stages 实际耗时(分钟)
Planning 计划 10
· Estimate · 估计这个任务需要多少时间 10
Development 开发 420
· Analysis · 需求分析 (包括学习新技术) 60
· Design Spec · 生成设计文档 120
· Design Review · 设计复审 (和同事审核设计文档) 10
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 10
· Design · 具体设计 60
· Coding · 具体编码 120
· Code Review · 代码复审 10
· Test · 测试(自我测试,修改代码,提交修改) 30
Reporting 报告 70
· Test Report · 测试报告 30
· Size Measurement · 计算工作量 10
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 30
合计 500

9、总结

  好久没用c++写小项目之类的东西了,现在写起c++还是有点别扭。还有就是犯了老毛病,计划的时候考虑得可能太多。我是本身比较讨厌改动和返工的人,所以在思考问题的时候总是容易想得过细,就容易产生过早的优化,比如我程序中用异或代替数交换,用掩码和位运算来做标志之类。就算这样做,毕竟还是没动手写,最后免不了还是要改动一些,而且可能本身对性能影响并不大。
  还有,总结写博客头好疼。组织语言好难,能准确表达自己的意思好难,能准确地让别人理解自己的意思难难难。
附:
  在老师提醒后修改了程序有大幅度提升,让我认识到,还是不能想当然,有思路就要尝试,万一效果拔群呢!