雅望的数独

1.项目地址

GitHub项目地址
GUI项目地址

2.PSP

PSP2.1Personal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划60120
Estimate估计这个任务需要多长时间1010
Analysis需求分析(包括学习新技术)300300
Design Spec生成设计文档60120
Design Review设计复审//
Coding Standard代码规范(为目前的开发制定合适的规范)6060
Design具体设计180180
Coding具体编码300420
Code Review代码复审120120
Test测试(自我测试,修改代码,提交修改)120240
Test Report测试报告6060
Postmortem&Process Improvement Plan事后总结,并提出改进计划180120
总计14501750

3.解题思路

根据题目进行需求分析,可以知道程序的主要功能有两点:

  1. 生成不重复数独终局(1<n<1000000),要求终局第一个数字固定,并按照格式输出至可执行文件同目录的文件。
  2. 求解数独(1<n<1000),题目以文本格式输入,输出要求与上一点相同,性能要求为60s。

除此之外,还有源代码管理、代码质量、单元测试等功能之外的需求。

最初的解题思路是,可以利用深搜来生成不重复的终局,这样第一个数字的条件也容易满足。但是由于有着性能要求,深搜的递归调用会严重的拖慢运行速度,所以深搜是肯定不行的。数独是有着明显规律的游戏,那么,是不是存在生成数独的规律?

抱着这样的目的去查询了一些资料发现,数独终局是有着很大的相似性的。例如,可以对终局进行数字替换、旋转、行列交换等操作,生成相似的终局。所以,现在的思路变为对原始终局进行变换,生成新的相似终局。

那么,106级别的终局,需要多少个原始终局才行呢?对数字进行替换实际上是对1-9进行全排列:假设原始数字按递增顺序排列,则可以将原始数字与全排列后的数字进行映射,以此进行替换。由于第一个数字要求不变,则数字替换可以带来 A 8 8 = 40320 A_8^8=40320 A88=40320种有效的终局。

对于行变换,由于数独有宫的要求,所以并不能随意的将行进行排列,只能在宫中(3行内)进行排列。再剔除第一行不允许改变,可以得到 2 × 6 × 6 = 72 2\times6\times6=72 2×6×6=72种可行终局。

令人开心的是,这两种变换之间并不矛盾。无论是先进行行变换还是数字替换,都不会产生重复终局,因此可以将这两种方式进行组合,可以得到 40320 × 72 = 2903040 40320\times72=2903040 40320×72=2903040种终局。所以,只需一个初始终局,进行行交换与数字替换即可满足需求,实现过程即为完成这两个矩阵变换。

至于求解数独,还是采用深搜+回溯的方式,对每个节点进行行、列、宫的判断,以尽量剪枝。由于题目不要求发现所有可行解,所以当发现一个解时即停止搜索。

4.设计实现过程

设计过程其实是一个取舍的过程。由于需求强调了性能要求,所以这里选择舍弃面向对象的涉及,而采用单纯函数与指针的方法,力求减少调用开销,提高运行速度。但是这也并不意味着全部牺牲了代码的可读性,在编写过程中基本参照了了Google C++的代码风格,并且简化单个函数,增加可读性。

4.1 生成数独终局

  1. 终局的数据结构
    本着从简、从速的原则,终局简单的利用二维数组char[9][9]保存,选择char是由于数独的合法范围小,已经足够。但实际上由于过程中不涉及以值的形式传递矩阵,所以这个优化并不明显。
    实际上,把矩阵这样暴露出来是很危险的,因为并没有办法保证矩阵中的数字都可以被合法的修改。一个更好的办法是把数独矩阵封装在一个类中,只给外部提供合法的改变途径,保证数据的安全性。这样的实现在后续GUI数独程序的设计中得到实现,而本程序为了减少开销,选择直接暴露。

  2. 数字的全排列
    std类库中提供了全排列的函数std::next_permutation(char *_First, char *_Last),每次调用对传入的数组按照从小至大的顺序调整一次。这样的调用方式已经非常方便,所以不再对其进行封装。在调用前,应该初始化char indexList[9]={1, 2, 3, 4, 5, 6, 7, 8, 9},再进行重复调用。
    但实际上选择初始化char indexList[9]={4, 1, 2, 3, 5, 6, 7, 8, 9},并在调用时从数组下标1开始(保证4的位置不被调整),这样只要初始矩阵的第一个数字为1,就可确保新矩阵的第一个数字符合要求。

  3. 行的交换
    行的交换分为两类。第一类为1-3行,只允许交换2、3行。第二类为4-6,7-9行,可以随意变换顺序。这种不同给实际编码会带来麻烦,因为逻辑的不一致需要额外的代码来实现。
    但是分析后可以发现,只对第二类进行变换产生的36种情况与数字变换进行组合,就已经看可以满足需求。所以可以不对第一类进行变换,使得代码更加简洁。

函数接口设计

//对数独行组中的line1与line2
//  group_addr:行组首地址,应为矩阵第1,3,6行首地址,实际仅应为第3,6行首地址
//  line1:被交换的行号,限制为0-2
//  line2:同上
void exchange2Line(char *group_addr, int line1, int line2);

//依据type对数独行组进行排列
//  group_addr:行组首地址,应为矩阵第1,3,6行首地址,实际仅应为第3,6行首地址
//  type:应为1-5,分别表示不同的排列方式:(021,102,120,201,210)
void switchLineInGroup(char *group_addr, int type);

//生成num个数独终局
void generateFinalMatrix(int num);

函数调用关系

调用
调用
调用
generateFianlMatrix
switchLineInGroup
next_permutation
exchange2Line

4.2 求解数独

  1. 对填数进行判断
    按照数独的要求进行行、列、宫的判断即可。若有重复则返回false,否则为true
  2. 深搜与回溯
    深搜以左上至右下的顺序进行递归调用,层数最多为81。
    每次DFS只负责完成当前位置的填数,之后递归调用,完成后面位置的填数。这也就意味着,当前位置的深搜完成代表之后的填数全部完成了。比如,若第40层的递归返回true,则代表40-81层的填数已经全部正确的完成了。此时返回上一次再次进行判断。

递归流程图

Created with Raphaël 2.2.0 开始 递归层数>81 返回true 当前位置是否需要填数 对当前位置填数 递归对下一位置填数 递归调用成功 递归对下一位置填数 递归调用成功 返回false yes no yes no yes no yes no

函数接口设计

//对矩阵pos位置填入key进行合法性判断
//  matrix:矩阵头指针
//  pos:左上至右下的数字位置,应为0-80
//  key:填入值,应为1-9
bool checkSinglePos(char matrix[9][9], int pos, char key);

//递归求解
bool findAnsByDFS(char matrix[9][9], int pos);

//对同目录下file_name的文件进行求解,输出至./sudoku.txt
int solveWholeProb(const char *file_name);

5. 代码说明

依照上文中的设计,给出程序中的关键代码。包括求解中的深搜与回溯生成终局的数字变换与行交换

//solve.cpp
//递归求解
bool findAnsByDFS(char matrix[9][9], int pos) {
 	if (pos > 81) return true;  //完成求解

	//当前位置是否需要填数
	if (matrix[pos / 9][pos % 9] != 0) {
		if (findAnsByDFS(matrix, pos + 1)) return true;
	}
	else {
		for (int i = 1; i <= 9; i++){
			if (checkSinglePos(matrix, pos, i) == true){  //填数
				matrix[pos / 9][pos % 9] = i;
				if (findAnsByDFS(matrix, pos + 1)) return true;  //对下一位置求解
				matrix[pos / 9][pos % 9] = 0;
			}
		}
	}
	return false;
}

//generate.cpp
//生成num个数独终局
void generateFinalMatrix(int num) {
	//origin matrix
	char seedMatrix[9][9] = {
			{ 1, 2, 3, 4, 5, 6, 7, 8, 9 },
			{ 4, 5, 6, 7, 8, 9, 1, 2, 3 },
			{ 7, 8, 9, 1, 2, 3, 4, 5, 6 },
			{ 2, 1, 4, 3, 6, 5, 8, 9, 7 },
			{ 3, 6, 5, 8, 9, 7, 2, 1, 4 },
			{ 8, 9, 7, 2, 1, 4, 3, 6, 5 },
			{ 5, 3, 1, 6, 4, 2, 9, 7, 8 },
			{ 6, 4, 2, 9, 7, 8, 5, 3, 1 },
			{ 9, 7, 8, 5, 3, 1, 6, 4, 2 } };
	
	//进行全排列的数组,第一个位置保持不变
	char indexList[] = { 4,1,2,3,5,6,7,8,9 };
	char output_temp[OUTPUT_TEMPLATE_SIZE];
	initOutputTemplate(output_temp);

	FILE *pf;
	fopen_s(&pf, "sudoku.txt", "w");
	int count = 0;
	while (next_permutation(indexList + 1, indexList + 9)) {  //全排列
		//change sequence of rows
		for (int i = 0; i <= 5; i++) {
			if (i != 0) switchLineInGroup(seedMatrix[3], i);  //4-6行互换
			for (int j = 0; j <= 5; j++) {
				if (j != 0) switchLineInGroup(seedMatrix[6], j);  //7-9行互换

				//change numbers
				for (int i = 0; i < 9; i++)
					for (int j = 0; j < 9; j++) {
						int pos = (i * 9 + j) * 2;
						output_temp[pos] = indexList[seedMatrix[i][j] - 1] + '0';
					}
				count++;

				//输出格式调整
				if (count == num) {
					output_temp[OUTPUT_TEMPLATE_SIZE - 3] = '\0';
					fputs(output_temp, pf);
					goto end;
				}
				else
					fputs(output_temp, pf);
			}
		}
	}
end:
	fclose(pf);
}

6.程序性能分析及改进

以生成106的终局进行性能分析,可以得到如下的报告
生成终局分析报告
可以发现生成终局的函数调用占据了大部分的时间。对函数内部进行分析可以发现,实际上最消耗时间的部分是文件输出。
函数内部分析报告然而,这已经是经过改进的输出方案了:在输出前提前准备好模板,只需将终局填入模板即可一次性的将矩阵输出,而不是一行一行的进行。与之前的方案相比,这样的方式可以提高50%左右的效率,百万级的终局输出在3s内可以完成。

7.单元测试

单元测试主要对上文设计的接口进行,包含了正例与错例,保证了接口在参数正确的情况下可以返回正确的结果。但是并不保证输入参数在范围以外的情况可以正确处理,因为假设函数调用者应该负责保证参数传递的正确性。
单元测试结果
如图可以看出,测试包含了生成终局中的所有函数,求解数独中的深搜部分。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值