数独(四)——测试与性能分析

我项目的GitHub地址:https://github.com/gmj0301/gmj
前面的博客有修改,请查看!

测试

该测试主要分为单元测试和正确性测试。

单元测试

程序分为 main.cpp,create.cpp,solve.cpp,create_sudo.h,solve_sudo.h 五个文件,分别实现对外接口,生成数独终局和解数独。具体思路已在前面的博客中完成。
函数关系如下:
在这里插入图片描述
因为outpu(),write(),main(),create(),solve() 可以直接通过文件,命令提示符查看便没有设计。对此,我根据剩余的 9 个函数设计了单元测试。
在这里插入图片描述
覆盖率如下所示:
在这里插入图片描述

正确性测试

生成数独终局

输入命令,依次生成1,10,100,1000个数独终局。时间都没有超过 60s ,结果如下:
在这里插入图片描述
在网上我找到了一个检查数独终局是否有重复的可执行文件,使用后发现,没有重复。
在这里插入图片描述
当输入指令不对时,显示如下:
在这里插入图片描述

解数独

输入命令,依次解1,10,100,1000个数独题目。
时间如下,没有超过60s:
在这里插入图片描述
解会输出到同目录下的 solve_sudo.txt 文件中,结果也正确,显示如下:
在这里插入图片描述

性能分析

生成数独终局

输入-c 100000 时,结果如下图所示:
在这里插入图片描述
通过上面的图可以看出,输出函数调用最多,占用时间最长,该函数代码如下:

void output(int sudo[9][9], FILE* fp)  //输出函数
{
	jiaohan_1(snum1, c1);
	jiaohan_2(snum2, c2); //判断当前两个参数表示哪两种顺序
	//输出数独
	for (int i = 0; i < 9; i++)
	{
		int k = i;
		if (k >= 3 && k <= 5) k = c1[k - 3]; //如果当前是中间三行,按照顺序输出
		else if (k > 5) k = c2[k - 6];       //如果当前是中间三行,按照顺序输出

		for (int j = 0; j <= 8; j++) {
			char x = sudo[k][j] + '0'; //转换成字符型
			fputc(x, fp);         //写入文件
			if (j != 8) fprintf(fp, " ");
		}
		fprintf(fp, "\n");
	}
}

经过测试,依次生成 1,10,100,1000,10000,100000,1000000 个数独终局,所需时间如下:
在这里插入图片描述

修改

经过思考,我把输出从一个个输出改成一行行输出。代码如下:

void output(int sudo[9][9], FILE* fp)  //输出函数
{
	jiaohan_1(snum1, c1);
	jiaohan_2(snum2, c2); //判断当前两个参数表示哪两种顺序

	char s[18];
	//输出数独
	for (int i = 0; i < 9; i++)
	{
		int k = i;
		if (k >= 3 && k <= 5) k = c1[k - 3]; //如果当前是中间三行,按照顺序输出
		else if (k > 5) k = c2[k - 6];       //如果当前是中间三行,按照顺序输出

		for (int j = 0; j <= 8; j++) {
			int kt = j * 2;
			s[kt] = sudo[k][j] + '0';
			s[kt + 1] = ' ';
			
		}
		s[17] = '\0';
		fputs(s, fp);         //写入文件,整行写入
		fprintf(fp, "\n");
	}
}

当生成 1000,10000,100000,1000000 个数独时,所用时间如下:
在这里插入图片描述
当生成 1e6 个数独时,从 42s 到只需要不到 8s 的时间,大大缩短了时间。

解数独

输入-s wert.txt 时,wert.txt 文本中有 10000 个数独题目,结果如下图所示:
在这里插入图片描述
通过上面的图可以看出,judge 函数和 solve_sudo 函数调用最多,占用时间最长,代码如下:

bool judge(int h, int l) {
	for (int i = 0; i < 9; i++)
		if (sudo[i][l] == sudo[h][l] && i != h)     //如果同一列存在与当前位置相同的数字,则出错
			return false;
	for (int i = 0; i < 9; i++)
		if (sudo[h][l] == sudo[h][i] && i != l)  //如果同一行存在与当前位置相同的数字,则出错
			return false;
	int x = (h / 3) * 3;
	int y = (l / 3) * 3;
	for (int i = x; i < x + 3; i++)
		for (int j = y; j < y + 3; j++)
			if (sudo[h][l] == sudo[i][j] && i != h && j != l)       
			//如果同一九宫格存在与当前位置相同的数字,则出错
				return false;
	return true;       //没有出现上述错误,则返回true
}
void solve_sudo(int i, int j) {
	if (judge_all == 1) return;

	int k, h, l, temp;
	for (k = sudo[i][j] + 1; k <= 9; k++) {
		sudo[i][j] = k;
		if (judge(i, j)) {    //当前位置可以填入数字i

			h = i;            //记录下当前位置旁边的下标[h,l]
			l = j + 1;
			temp = 0;
			for (; h < 9; h++) {
				for (; l < 9; l++)
					if (sudo[h][l] == 0) {   //如果[h,l]处数字为0
						temp = 1;            //temp=1标记
						break;
					}
				if (temp == 1) break;
				else l = 0;
			}
			if (temp == 1) solve_sudo(h, l);  //[h,l]处数字为0,调用solve_sudo函数
			else {
				judge_all = 1;              //所有位置都不为0,说明数独题目解完了
				return;                     //返回
			}
			if (judge_all == 1) return;     //返回
		}

	}
	//1-9这9个数字都填完了,没有符合的数字
	//说明上一个位置出的数字填错了,当前位置置0,回到上一个位置
	sudo[i][j] = 0;
	return;
}

经过测试,依次解 1,10,100,1000,10000,100000,1000000 个数独题目,所需时间如下:
在这里插入图片描述

修改

经过思考,把剪枝进行优化。代码如下:

bool judge(int h, int l) {
	for (int i = 0; i < 9; i++)
		if ((sudo[i][l] == sudo[h][l] && i != h) ||( sudo[h][l] == sudo[h][i] && i != l))       
		//如果同一行、同一列存在与当前位置相同的数字,则出错
		{
			return false;
		}
	int x = (h / 3) * 3;
	int y = (l / 3) * 3;
	for (int i = x; i < x + 3; i++)
		for (int j = y; j < y + 3; j++)
			if (sudo[h][l] == sudo[i][j] && i != h && j != l)       
			//如果同一九宫格存在与当前位置相同的数字,则出错
				return false;
	return true;       //没有出现上述错误,则返回true
}

深搜时,从列的角度思考,哪列的挖空数目最少,从哪列开始搜索,这样可以减少调用 judge 函数的次数,也可以减少调用 solve_sudo 的次数,从而到达减少时间。代码如下:

int serch() //找哪列空白个数最少
{
	int min = 0, first = 0;
	for (int i = 0; i < 9; i++)
	{
		if (numl[i] != 0 && first == 0) //0个空白就说明这列不用填,略过
		{
			min = i;
			first = 1;
		}
		if (numl[i] < numl[min] && numl[i] != 0 && first == 1)
			min = i;
	}
	if (first == 0) return -1; //都是0,返回-1
	else return min;
}

int mmin(int j) //求这一列第一个是0的下标
{
	int res = 0;
	for (int i = 0; i < 9; i++)
		if (res == 0 && sudo[i][j] == 0)
			return i;
	return -1;
}
void solve_sudo(int i, int j) {
	if (judge_all == 1) return;

	int k, h;
	numl[j]--;        //当前列的空白个数减一
	for (k = sudo[i][j] + 1; k <= 9; k++) {
		sudo[i][j] = k;
		if (judge(i, j)) {    //当前位置可以填入数字i
			if (numl[j] > 0)   //当前列还有空白
				solve_sudo(mmin(j), j);
			else 
			{
				h = serch();
				if (h == -1)
				{
					judge_all = 1;              //所有列的空白个数均为0,说明数独题目解完了
					return;                     //返回
				}
				else solve_sudo(mmin(h), h);
			}
			if (judge_all == 1) return;     //返回
		}

	}
	//1-9这9个数字都填完了,没有符合的数字
	//说明上一个位置出的数字填错了,当前位置置0,回到上一个位置
	sudo[i][j] = 0;
	numl[j]++;
	return;
}

除此之外,输出时也从一个个输出变成了一行行输出。
当解 1,10,100,1000,10000,100000,1000000 个数独时,所用时间如下:
在这里插入图片描述
可以看到,解 1000 个数独题目没有太大变化,当解 1e4 ~ 1e6 个数独时,时间明显缩短了。优化后,解 1e4 个数独只需要 11s 左右,解 1e5 个数独只需要 2min 左右,解 1e6 个数独需要 20min 左右,是之前的一半,优化效果很明显。
修改后,单元测试部分也进行了修改,也通过了:
在这里插入图片描述
当然,这不是最好的优化结果,还有待提高。可以使用其他更好的剪枝方法和解方法,减少递归使用;输入输出上,也可以缩短读取和写入文本的时间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值