软件工程个人项目——1120161755

目录

GitHub项目地址

PanQuixote/数独.

PSP表格

在这里插入图片描述

题目要求

  • 数独的概念
    下图为数独题目的示意图,数独要求任意某一行或者某一列都不存在重复的数字。图中的粗线将数独题目划分为9个区域,这些区域称为宫。每个宫内也不允许存在重复的数字。
    数独示意图

  • 生成终局
    1、在命令行中使用参数-c和数字N(1<=N<=1000000)生成N个数独终局。
    2、生成的数独终局保存在一个txt中,每次生成的txt覆盖上次生成的txt文件。生成的数独终局,每行每个数字间用一个空格分隔,行末无空格。终局与终局间空一行。

  • 求解数独
    1、在命令行中使用参数-s和文件名来求解数独,要求从该文件中读取题目并求解,输出结果到与此程序相同目录下的sudoku.txt中。
    2、数独题目个数N满足1<=N<=1000000,格式同数独终局。

解题思路描述

  • 生成数独终局
    查阅网上资料后发现生成数独终局的方法大概有如下几种:

   1、回溯法。随机生成数字填入当前网格中,检测是否符合要求,如果符合则继续生成数字填到下一个网格,否则回溯到上一步,不断随机生成数字直到所有网格被填满。
   2、模板法。事先准备好一个符合要求的数独终局模板,模板中的元素为字母a~i,然后随机生成一串包含1-9的数字序列,将字母一一对应,替换为数字,获得数独终局。
   3、数列法。生成一个数列,然后将此数列分别向右移动0、3、6、1、4、7、2、5、8位得到终局第一到第九行。可以将第1~3行对应的移动位数改变一下顺序,如改为0、6、3,就可以得到一个新终局。此外,4到6,7到9行也可以更改。于是一个数列可以生成6^3=216个终局,数字1到9可以生成9!= 362880个数列。

   显然,方法一生成的数独终局随机性非常高,但是效率非常低,如果要生成大量的数独终局,将花费大量的时间和计算机资源;而方法二虽然效率高,但是如果只使用一个模板的话,最多能生成的不同的数独终局数仅为9!=362880,而本项目中规定数独终局的第一行第一列为定值,使一个模板能生成的不同的数独终局数为8!=40320,显然是远远无法达到题目要求的数量上限(N<=1000000)的。此外,在生成数列的过程中,容易产生相同的数列,导致实际生成的数独终局更少;方法三效率非常高,而且可以保证生成的终局中不存在重复的终局,缺点是生成的终局结构非常相似。

   本次实验采用了方法三。由于实验要求终局的第一位为固定值,所以在生成数列时先固定第一位的数字。实验要求的最大终局数为100万,而8个数字的全排列数为8!约为4万,也就是说一个数列最多只需生成100/4=25个终局。本次实验中每个数列生成30个终局。生成终局的思路如下:
   1、设定一个初始数列为当前数列。
   2、由当前数列生成一个全排列,当前数列设置为全排列,由当前数列生成30个数独终局。
   3、检查生成的终局数是否达到要求,如果没达到,返回第二步,否则退出。

  • 求解数独
      使用递归方法。递归函数为 void place_num(int sudo[][9], int pos, int num)。其中sudo为当前的数独题。pos 为当前将要放置的位置(0~80),num为将要放置的数字。递归的过程如下:

  1、如果 pos>80,跳到第三步。

  2、如果 0<=pos<=80 将 sudo[pos/9][pos%9] 设置为 num。将pos置为下一个空白位置对应的位置号。尝试将数字1~9填入位置pos中,检测是否符合数独的要求。如果符合,递归调用此函数,调用方式为 place_num(int sudo[][9], int pos, int num),其中sudo和pos的值均为修改后的值。

  3、输出当前数独sudo。

设计和实现过程

生成终局流程图如下:
在这里插入图片描述

  • 生成终局用到的一些主要函数:
//记录移动的方式。每行记录生成一个数独时需要进行的操作。
//例如,第一行 0,3,6,1,4,7,2,5,8 表示,数独第一行由数列se向右移动0位得到;数独第二行由se向右移动3位得到……
//此数组共有30行,代表通过一个数列可以获得30个数独。
int move_way[30][9] = {
	{ 0,3,6,1,4,7,2,5,8 },
	{ 0,3,6,1,7,4,2,5,8 },
	{ 0,3,6,4,1,7,2,5,8 },
	{ 0,3,6,4,7,1,2,5,8 },
	{ 0,3,6,7,1,4,2,5,8 },


	{ 0,3,6,1,4,7,2,8,5 },
	{ 0,3,6,1,4,7,5,2,8 },
	{ 0,3,6,1,4,7,5,8,2 },
	{ 0,3,6,1,4,7,8,2,5 },
	{ 0,3,6,1,4,7,8,5,2 },

	{ 0,3,6,1,7,4,2,8,5 },
	{ 0,3,6,4,1,7,5,2,8 },
	{ 0,3,6,4,7,1,5,8,2 },
	{ 0,3,6,7,4,1,8,2,5 },
	{ 0,3,6,7,1,4,8,5,2 },

	{ 0,6,3,1,4,7,2,5,8 },
	{ 0,6,3,1,7,4,2,5,8 },
	{ 0,6,3,4,1,7,2,5,8 },
	{ 0,6,3,4,7,1,2,5,8 },
	{ 0,6,3,7,1,4,2,5,8 },


	{ 0,6,3,1,4,7,2,8,5 },
	{ 0,6,3,1,4,7,5,2,8 },
	{ 0,6,3,1,4,7,5,8,2 },
	{ 0,6,3,1,4,7,8,2,5 },
	{ 0,6,3,1,4,7,8,5,2 },

	{ 0,6,3,1,7,4,2,8,5 },
	{ 0,6,3,4,1,7,5,2,8 },
	{ 0,6,3,4,7,1,5,8,2 },
	{ 0,6,3,7,4,1,8,2,5 },
	{ 0,6,3,7,1,4,8,5,2 },
};

//将数列se向右移动n位,将移动后的结果转化为符合要求的格式并存入result
void move_se(char* se, char* result, int n)

//生成N个数独到文件file_name中。如果generate_way = 0则生成终局,为1生成数独题
int generate_sudoku(int N, char* file_name, int generate_way)

求解数独流程图:
在这里插入图片描述

  • 求解数独用到的一些主要函数:
//递归,将num放置到空位置pos(0~80)上,直到所有的空位置被填满,填好的数独存在result中  
void place_num(int sudo[][9], int pos, int num, int result[][9])  
  
//判断在sudo[x][y]上放置数字num是否符合规则,是则返回1,否则返回0  
int is_suit(int sudo[][9], int x, int y, int num)  

代码改进及性能测试

本次实验只要求测试生成终局的性能
  - 原始代码的CPU使用率及函数占用的CPU函数分析:
  本次测试的参数为-c 10000
在这里插入图片描述
在这里插入图片描述
  可以看出,用于输出数独终局至文件的函数print_sudoku占用了很大一部分的CPU时间。因而接下来将对此函数进行改进。
  原始代码使用fprintf进行输出,尝试着修改为使用fputs进行输出。修改后使用参数-c 100000对两版代码进行测试,统计了运行时间,结果如下:
在这里插入图片描述
  可以看出,使用fputs输出会使程序性能得到较大幅度的提高。因而重新修改了代码。
  但是经过与他人的程序性能进行对比,发现我的程序性能大概只有其他人的百分之一。经过研究及测试后发现,这是由于我的代码中频繁开闭文件导致的。于是将原来的输出函数print_sudoku整合到其他函数中去(因为在此函数中,需要打开并关闭文件,之前是生成一个终局就调用一次此函数)。除此之外,原程序每次输出只一个数字。
  于是将程序修改为保持文件始终打开,每生成一个终局就将终局修改为一长串符合格式的字符串,然后将字符串输出到文件中。修改后的程序生成1000000个终局大约需要5~6s,但是这样修改的缺陷是模块化程度降低。
  尽管此时的程序性能已经较高,但是与其他同学的(生成100万终局需2~3s)相比还是有着较大的差距。于是开始思考是否还有能够再进行优化的地方。经过一段时间的研究及代码测试,发现有许多时间是花费在转化格式的过程中了。在之前,我的做法是,通过移动数列生成终局的一行后,先将其转化为符合要求的格式(每个数字之间有空格,行末有换行符),再将其拼接到用于表示整个数独终局的字符串中去。修改之后的做法是,在移动数列的过程中就把数列转化成相应格式,直接写入数独终局字符串,于是便省去了许多转化过程。修改后的代码生成100万终局大约需要1.5到2秒,性能大大提高。

  接下来是对改进后的代码的测试:
CPU占用率测试:
参数为-c 1000000
在这里插入图片描述
可以看到,cpu占用率大约在25~30之间。多次测试,运行时间在1.5到2s之间。耗时最多的函数为generate_suodku(),详细代码在“关键代码说明”部分。

单元测试
测试的函数如下:

//将数列se向右移动n位,将移动后的结果转化为符合要求的格式并存入result
void move_se(char* se, char* result, int n)
{
	for (int i = 0; i < n; i++)
	{
		result[i * 2] = se[9 - n + i];
		result[i * 2 + 1] = ' ';
	}

	for (int i = n; i < 9; i++)
	{
		result[i * 2] = se[i - n];
		result[i * 2 + 1] = ' ';
	}
	result[17] = '\n';
}

单元测试的代码如下:

#include "stdafx.h"
#include "CppUnitTest.h"
#include "测试函数.h"
using namespace Microsoft::VisualStudio::CppUnitTestFramework;

namespace UnitTest1
{		
	TEST_CLASS(UnitTest1)
	{
	public:
		
		TEST_METHOD(Test_Move_se)
		{
			//将se向右移动三位并转化为符合要求的格式

			char se[9] = { '2','1','3','4','5','6','7','8','9' };

			//预期的结果
			char expect_string[18] = { '7',' ','8',' ','9',' ','2',' ','1',' ','3',' ','4',' ','5',' ','6','\n' };
			
			//运行的结果
			char result_string[18] = { 0 };
			move_se(se, result_string, 3);

			for (int i = 0; i <= 18; i++)
				Assert::AreEqual(result_string[i], expect_string[i]);

		}
	
	};
}

测试结果截图如下:
在这里插入图片描述

关键代码说明

  • 生成终局
//生成N个数独到文件file_name中。如果generate_way = 0则生成终局,为1生成数独题
int generate_sudoku(int N, char* file_name, int generate_way)
{
	FILE *fp;
	errno_t open_error = fopen_s(&fp, file_name, "w");//打开成功返回非零,失败返回0

	char se[9] = { '2','1','3','4','5','6','7','8','9' };//学号后两位为5、5,(5+5)%9+1=2,所以数列首位为2


	int sudoku_sum = 0;//已生成的数独终局数

	while (1)
	{
		next_permutation(&se[1], &se[1] + 8);//对se的第二位到第九位进行全排列变换,得到一个新数列

		for (int i = 0; i < 30; i++)//对每个数列,生成30个数独
		{
			char sudoku_string[18 * 9 + 1] = { 0 };//一个数独的字符串形式

			for (int j = 0; j < 9; j++)
			{
				//按照移动表来移动数列se,移动后的结果拼接在sudoku_string后面
				move_se(se, &sudoku_string[strlen(sudoku_string)], move_way[i][j]);
			}
			sudoku_string[18 * 9] = '\n';

			if (generate_way == 1)
				change_into_problem(sudoku_string);


			sudoku_sum++;

			if (sudoku_sum == N)//已生成足够数目的数独
			{
				sudoku_string[18 * 9 - 1] = '\0';
				sudoku_string[18 * 9] = '\0';
				fputs(sudoku_string, fp);//输出数独字符串到文件
				fclose(fp);
				return N;
			}

			fputs(sudoku_string, fp);//输出数独字符串到文件
		}

	}

}

  • 求解数独
//递归,将num放置到空位置pos(0~80)上,直到所有的空位置被填满,填好的数独存在result中
void place_num(int sudo[][9], int pos, int num, int result[][9])
{
	int copy[9][9] = { 0 };
	copy_sudo(sudo, copy);

	if (pos >= 0)//当前位置合法,将此位置置为num
		copy[pos / 9][pos % 9] = num;

	//找到下一个对应的数字为0的位置
	do
	{
		pos++;
		if (pos > 80)//当前数独已是终局
		{
			copy_sudo(copy, result);
			return;
		}
	} while (copy[pos / 9][pos % 9] != 0);

	//尝试将此位置的下一位置置为n,n的范围是1~9
	for (int n = 1; n <= 9; n++)
	{
		if (s_is_suit(copy, (pos / 9) , (pos % 9) , n) == 1)//如果当前位置置为n合适,则递归设置下一个为0的位置
		{
			place_num(copy, pos, n, result);
		}
	}
}

附加题

要求:为数独游戏做一个GUI界面。

设计和实现过程:
  创建MFC程序来实现此要求。由于已经有生成数独题的函数了,只需将生成的题目展现在界面上即可。实现过程比较简单,拖动文本框和按钮控件到界面上,然后为相应的按钮响应函数添加代码即可。最终的界面如下:

在这里插入图片描述
  界面上有五个按钮。
  其中“生成数独题按钮”可以在网格中生成一个数独题。点击后界面变成这样:
在这里插入图片描述
  然后用户可以在空白处填写数字。填写过程中可以自由修改填写的内容,也可以点击“清除输入”按钮清除输入的数字。随意填写几个数字,点击提交:可以看到显示数独填写错误。
在这里插入图片描述
点击清除输入:恢复到一开始的状态。
在这里插入图片描述
点击显示答案:可以看到答案显示在网格中。
在这里插入图片描述
再点击一次提交。可以看到此时网格中的数独是正确的。
在这里插入图片描述
点击退出按钮,退出游戏。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值