sudoku数独软件

Github项目链接

PSP2.1Personal Software Process Stage预估耗时(分钟)实际耗时(分钟)
Planning计划6030
·Estimate·估计这个任务需要多少时间1010
Development开发12001200
·Analysis·需求分析(包括学习新技术)300300
·Design Spec·生成设计文档6060
·Design Review·设计复审3030
·Code Standard·代码规范(为当前的开发制定合适的规范)3030
·Design·具体设计200250
·Coding·具体编码400450
·Code Review·代码复审3030
·Test·测试(自我测试、修改代码、提交修改)600700
Reporting报告12060
·Test Report·测试报告120180
·Size Measurement·计算工作量2030
·Postmortem & Process Improvement Plan·事后总结,并提出改进计划6060
合计20402220

一、解题思路

1.1 实现生成数独终局

实现生成数独有两个方案。
第一种方案是暴力枚举,这种方法理论上能够生成所有种类的数独终局,但是时间复杂度太高,在性能上表现很差。
第二种方案是随机生成 1 ~ 9 的某种全排列顺序,然后通过某种变换生成最终的数独。这种方案十分快速,生成一个数独终局的时间复杂度基本上是 O(1) 的,只不过是常数很大。但这种方案有个缺点,就是它能够生成的数独终局的数量很少,只有 9!个,而第一种方案有6,670,903,752,021,072,936,960(约为6.67×10的21次方)种组合。但是在此项目中,要求1000个内基本无重复是完全足够了。
所以我们采用第二种方案。

1.2 实现数独求解

数独求解只能暴力dfs搜索,我目前没有找到理论上时间复杂度更优的算法。

二、设计实现

2.1 generate 函数

generate 函数生成给定的数量的数独终局到指定文件中。

2.2 game 类

game 类包含一个solution方法,该方法功能为求解数独。
game 类还包含两个private成员函数 game::dfs()(实现深度优先搜索)和 game::get_index()(具体功能在后面)。

2.3 processinput 函数

项目需求中要求通过cmd指令sudoku.exe -c 1000来生成一定数量的数独终局。
以及sudoku.exe -s absolute_path_of_puzzlefile来求解指定文件中的数独终局。
processinput功能为:处理输入指令,并调用相应的函数。

2.5 单元测试设计

通过设计测试用例,使得代码覆盖率达到92.5%。

三、性能分析

3.1. 解数独功能的性能分析

我们使用一个较强的用例

0 0 0 0 0 5 0 2 0
1 0 0 9 0 0 0 0 0
4 0 0 0 0 0 0 0 0
0 0 8 0 0 2 0 0 0
0 0 0 0 0 0 0 0 1
0 0 0 0 0 0 9 0 4
0 0 5 0 0 0 3 0 0
0 0 0 1 0 0 0 0 7
0 0 2 4 0 8 0 0 0

我们先运行一下,看一下程序运行时间。
在这里插入图片描述
34.263秒
非常的差劲。
我们再回过头看一下solve_puzzle的代码,可以发现,设作标志的变量太多,整个循环计算太繁琐。看一下调用的jdg_rep函数(用来判断数独上某个空是否可以填某个数字)。

int jdg_rep(int row, int line, int putnum, int sudoku[9][9])
{
	for (int i = 0; i < 9; i++)
	{
		if (i == row) continue;
		if (sudoku[i][line] == putnum)
			return false;
	}
	for (int i = 0; i < 9; i++)
	{
		if (i == line) continue;
		if (sudoku[row][i] == putnum)
			return false;
	}
	for (int i = row - row % 3; i < row - row % 3 + 3; i++)
	{
		for (int j = line - line % 3; j < line - line % 3 + 3; j++)
		{
			if (i == row && j == line) continue;
			if (sudoku[i][j] == putnum) return false;
		}
	}
	return true;
}

可以发现每个操作都是必须的,jdg_rep函数没法继续优化。而且整个DFS因为之前已经做过剪枝(未展示代码),所以算法上也无法进行优化。能够做的只有简化DFS的代码实现的逻辑(减少过多的状态变量以及状态数组或更改为递归实现。)。
简化代码,并使用位运算
如果我们把int变量的最低9位用作表示1~9九个数字,那么很多操作都会简化。
具体表示方法为:
32位的 int 变量从最低位到最高位,若前九位任意一位为1,则这个int变量就代表哪个数字,如果有一位为1,则其余位全为0。具体思想类似one-hot编码方式
优化方法:
如果判定数独的某个空可不可以填入某个数字,需要查看和该空同一行、同一列、同一 3 x 3矩阵内有没有相同的数字。则可以将同行、同列、同框内的所有数字按位取反,将亟待判断的位置的值记作 x,置为 0…0111111111(2e9 - 1),然后将 x 与所有已经按位取反的数字进行按位与操作,最后 x 的低 9 位哪里为1,则 x 可以取值为几。如:若 x = 0…0000000001,则该位置只能填1;若 x = 0…111111111,则该位置能填1~9任何一个数。参考代码
优化后的时间
在这里插入图片描述
2.614秒
速度快了很多。

3.2 进行生成数独部分的优化

我们生成10000个数独,命令:

./sudoku.exe -c 10000

启动性能探查器 - CPU使用率
分析结果:
在这里插入图片描述
双击process_input函数
在这里插入图片描述
不出所料,使用CPU最多的还是生成数独的函数。
查看函数内部CPU使用情况。
在这里插入图片描述
可以看到占用CPU最多的是文件的写入,占用了90.7%。
所以生成数独部分代码即便得到优化,效果也不会太明显。
所以通过减少调用fprintf函数来达到优化时间的目的。
优化前代码:

for (int i = 0; i < 9; i++) {
	for (int j = 0; j < 9; j++) {
		if (num == 0 && i == 8 && j == 8)
			fprintf(file, "%d", sudoku[i][j]);
		else
			fprintf(file, "%d%c", sudoku[i][j], j == 8 ? '\n' : ' ');
	}
}

优化前生成用时为
在这里插入图片描述
10000个用时2.235秒,1000000用时21.984秒。
优化后代码:

for (int i = 0; i < 9; i++) {
			if (i == 8) 
				fprintf(file, "%d %d %d %d %d %d %d %d", sudoku[i][0], sudoku[i][1], sudoku[i][2], sudoku[i][3], sudoku[i][4], sudoku[i][5], sudoku[i][6], sudoku[i][7]);
			else 
				fprintf(file, "%d %d %d %d %d %d %d %d\n", sudoku[i][0], sudoku[i][1], sudoku[i][2], sudoku[i][3], sudoku[i][4], sudoku[i][5], sudoku[i][6], sudoku[i][7]);
		}

优化后用时
在这里插入图片描述

四、代码说明

4.1 generate 函数

核心代码:

//洗牌算法随机生成左上角 3 x 3小矩阵
for (int i = 0; i < 20; i++)
{
	int a = rand() % 8 + 1;
	int b = rand() % 8 + 1;
	int tmp = sudoku[a / 3][a % 3];
	sudoku[a / 3][a % 3] = sudoku[b / 3][b % 3];
	sudoku[b / 3][b % 3] = tmp;
}
for (int a = 0; a < 3; a++) {
	for (int b = 0; b < 3; b++) {
		//枚举每个 3 x 3 小矩阵
		if (a == 0 && b == 0) continue;
		//通过将左上角的小矩阵进行行列变换生成其余8个矩阵
		for (int i = 0; i < 3; i++) {
			for (int j = 0; j < 3; j++) {
				int row = i + a * 3;
				int line = j + b * 3;
				sudoku[row][line] = sudoku[(row + b) % 3][(line + a) % 3];
			}
		}
	}
}

代码思路:
首先通过洗牌算法,随机生成一个 3x3 的矩阵,即为数独最左上角的小矩阵。然后通过行列变换生成其余8个 3x3 的小矩阵。变换方式为:将行数和列数每次加1模3。变换次数为该矩阵离左上角小矩阵的水平和竖直距离。

4.2 game class

处理矩阵:

for (int i = 0; i < 9; i++) {
	for (int j = 0; j < 9; j++) {
		//读文件
		int end = fscanf_s(readfile, "%d%c", &number_temp, &temp, (unsigned int)(sizeof(int) + sizeof(char)));
		//如果是空位
		if (number_temp == 0) vec.push_back(pair<int, int>(i, j));
		//如果是数字
		else sudoku[i][j] = 1 << (number_temp - 1);
		if (end == EOF) flagtobreak = 1;
	}
}

深搜:

int game::dfs(int now)
{
	int row = vec[now].first;
	int line = vec[now].second;
	//查询可以填入的数字
	int f = (1 << 9) - 1;
	for (int i = 0; i < 9; i++) {
		f &= (~sudoku[row][i]) & ((~sudoku[i][line]) & (~sudoku[row / 3 * 3 + i / 3][line / 3 * 3 + i % 3]));
	}
	//枚举可以填入的数字
	while (f)
	{
		//取出数字
		sudoku[row][line] = f & (-f);
		//清除该数字
		f &= ~sudoku[row][line];
		//递归
		if (now < vec.size() - 1 && !dfs(now + 1)) continue;
		else return true;
	}
	//没有可以填入的数字
	sudoku[row][line] = 0;
	return false;
}

转换为整形

int game::get_index(int num)
{
	int ans = 0;
	while (num) {
		ans++;
		num >>= 1;
	}
	return ans;
}
代码详细思路:

(1)表示方法: one-hot编码。例如,数字1表示为

00000000 00000000 00000000 00000001

数字5表示为

00000000 00000000 00000000 00010000

(2)查询某空位能填入什么数字的方法:
首先将变量 f,通过操作 f = (1 << 9) - 1置为

00000000 00000000 00000001 11111111

让后让 f 和与所查询空位同行、同列、同 3x3 小矩阵中的其余数字的按位取反后的值进行按位与操作。最后 f 中剩下的值为1的位即为可以填入的数字。
例:
若同行、同列、同矩阵中有数字5,表示为:

00000000 00000000 00000000 00010000

按位取反后表示为:

11111111 11111111 11111111 11101111

在与 f 进行按位与操作后,f 的值变为:

00000000 00000000 00000001 11101111

从右往左第5位变为0,则此位置不能填5。
若同行、列、矩阵中有 1 ~ 9 所有数字,则 f 值变为 0,即此空位无可填入的数字。

五、项目总结

通过此次项目,我熟悉了个人项目的流程,理解了软件工程的很多概念。深刻理解了一个好的设计、规范的代码风格以及规范的文件组织对于整个软件生命周期的影响。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值