软件工程基础——个人项目(解数独)

Github项目地址:https://github.com/2016bits/sudoku.git

附加题Github地址:https://github.com/2016bits/interface_of_sudoku.git

 

一、题目描述:

实现一个能够生成数独终局并且能求解数独问题的控制台程序。

 

二、要求描述:

实现一个命令行程序:

  1. 生成不重复的数独终局至文件;
  2. 读取文件中的数独问题,求解并将结果输出到文件

生成终局:

  1. 在命令行中使用-c参数加数字N(1<=N<=1000000)控制生成数独终局的数量,例如下述命令将生成20个数独终局至文件中:
    sudoku.exe -c 20
  2. 将生成的数独终局用一个文本文件(假设名叫sudoku.txt)的形式保存起来,每次生成的txt文件需要覆盖上次的txt文件,格式如下,数与数之间由空格分开,终局与终局之间空一行,行末无空行:
    2 6 8 4 7 3 9 5 1
    3 4 1 9 6 5 2 7 8
    7 9 5 8 1 2 3 6 4
    5 7 4 6 2 1 8 3 9
    1 3 9 5 4 8 6 2 7
    8 2 6 3 9 7 4 1 5
    9 1 7 2 8 6 5 4 3
    6 8 3 1 5 4 7 9 2
    4 5 2 7 3 9 1 8 6
  3. 程序在处理命令行参数时,不仅能处理正确的参数,还能处理各种异常的情况,如:
    sudoku.exe -c abc
  4. 在生成数独矩阵时,左上角的第一个数为:(学号后两位相加)% 9 + 1。例如学生A学号后两位是80,则该数字为(8 + 0)% 9 + 1,那么生成的棋盘如下(x表示满足数独规则的任意数字):
    9 x x x x x x x x
    x x x x x x x x x
    x x x x x x x x x
    x x x x x x x x x
    x x x x x x x x x
    x x x x x x x x x
    x x x x x x x x x
    x x x x x x x x x
    x x x x x x x x x

 

求解数独:

  1. 在命令行中使用-s参数加文件名的形式求解数独,并将结果输出至文件,如:
    sudoku.exe -s absolute_path_of_puzzlefile
  2. 格式如下,其中0代表空格,题目与题目之间空一行,行末无空格,最后一个数独题目后无空行:
    9 0 8 0 6 0 1 2 4
    2 3 7 4 5 1 9 6 8
    1 4 6 0 2 0 3 5 7
    0 1 2 0 7 0 5 9 3
    0 7 3 0 1 0 4 8 2
    4 8 0 0 0 5 6 0 1
    7 0 4 5 9 0 8 1 6
    8 9 0 7 4 6 2 0 0
    3 0 5 0 8 0 7 0 9
  3. sudoku.txt的格式如下(与生成终局的要求相同):
    9 5 8 3 6 7 1 2 4
    2 3 7 4 5 1 9 6 8
    1 4 6 9 2 8 3 5 7
    6 1 2 8 7 4 5 9 3
    5 7 3 6 1 9 4 8 2
    4 8 9 2 3 5 6 7 1
    7 2 4 5 9 3 8 1 6
    8 9 1 7 4 6 2 3 5
    3 6 5 1 8 2 7 4 9
  4. 数独题目个数N(1<=N<=1000000),保证数独格式正确。

 

三、时间估计:

PSP2.1 Personal Software Process Stages 预估耗时 实际耗时
Estimate 估计这个任务需要多少时间 1month 15days
Analysis 需求分析(包括学习新技术) 7days 1days
Design Spec 生成设计文档 3days 1days
Design Review 设计复核(审核设计文档) 3days 1days
Coding Standard 代码规范(为目前的开发制定合适的规范) 1days 2days
Design 具体设计 10days 1days
Code Review 具体编码 1days 6days
Test 测试 3days 1days
Others 其他 2-3days 2days

 

四、解题思路:

先审题,发现该题目包括两个部分:生成终局和求解数独,这两部分可以完全分开来做,其公用部分为输入、输出和数独规则的判断,所以可以将其分为两个不同的函数来执行。首先,实现输入的部分:本题与以往题目不同的地方就在于其输入,本题是通过在终端用命令调用exe文件,同时在此过程中传递参数。经百度,了解到main(int argc, char* argv[])中的参数argc和argv就是用来接收终端传来的参数,argc表示参数的个数,argv表示参数的数组。同时,该程序的输入和输出都是在文件中进行的,百度后了解到要用头文件fstream,用ifstream和ofstream来实现输入输出。下面具体分析生成终局和求解数独的做法:

生成终局:

如果完全按照搜索来做,复杂度会相当高,所以查阅相关资料后,结合数独的相关特点,总结了以下几点:

  1. 每行生成一个1-9的排列,从而保证行里的数字不会重复;
  2. 然后将下一行进行平移(平移距离不为0),从而保证列里的数字不会重复;
  3. 前三行的平移距离为0、3、6(036或063两种情况),接下来三行平移1、4、7(147、174、417、471、714、741六种情况),最后三行平移2、5、8(258、285、528、582、825、852六种情况),从而保证了每个3*3方格里的数字不会重复,且总方案数2*6*6*8!= 2903040 > 10^6,满足题目要求。

求解数独:

暂时除了深搜没有太好的方法,所以用一个结构体(或类)保存所有空格的行、列、3*3方阵,从而一个个进行试探,如果1-9均不合适,则返回上一层,若到达最后一个数字也没有冲突,则输出该结果。

五、流程图:

软件工程基础——个人项目(解数独)_第1张图片

 

六、具体实现:

总体采用了面向对象的思想:

  1. 设计了一个名为Sudo的类,实现生成终局的函数Finality和实现解数独的函数Solution作为该类的方法,以及在实现生成终局时移位的函数Movesudo和实现解数独时生成时的搜索函数Tryspace也作为该类的方法;
  2. 将输入输出的文件变量fin和fout作为该类的属性,以及解数独时的空格结构体space和空格的数量space_num和存放数独的题目数组array作为该类的属性。

在主函数里面新建对象sudo,通过命令行传来的参数来判断对象具体调用的方法。

 

七、性能分析:

  1. 最初的版本使用ofstream来将生成的终局输出到文件,在dev上生成的.exe文件生成1000000个终局需要二十多秒,在vs上需要两分半;之后将输出改成fputc之后,在dev上生成.exe文件生成1000000个终局需要四秒多,在vs上需要十三秒多。可以说,代码的性能得到了很大的提升。
  2. 最初在找到一个解后没有返回,所以速度极慢;之后加入了判断,用变量ok来标记是否全部试探完毕,之后速度快多了。

软件工程基础——个人项目(解数独)_第2张图片

软件工程基础——个人项目(解数独)_第3张图片

 

七、主要代码:

生成移动规则并遍历:

char rule0[10][5] = {"036", "063"};
char rule1[10][5] = {"258", "285", "528", "582", "825", "852"};
char rule2[10][5] = {"147", "174", "417", "471", "714", "741"};
int count = 0;					//记录终局个数
bool flag = true;
do {
	a[8] = '3';					//插入学号 :(2+0)% 9 + 1 = 3 
	//平移的2*6*6种方式
	for (int i = 0; i < 2 && flag; ++i) {
		for (int j = 0; j < 6 && flag; ++j) {
			for (int k = 0; k < 6 && flag; ++k) {
				Movesudo(rule0[i], rule1[j], rule2[k], a, fout);
				++count;
				if (count >= num) {
					flag = false;
					break;
				}
			}
		}
	}
} while (next_permutation(a, a + 8));			//使用STL全排列函数

根据移动规则将终局输出到文件:

//开始移位并存储在文件中
	
for (int i = 0; i < 9; ++i) {
	int step = steps[i];
	fputc(a[(8+step)%9], fout); 
	for (int j = 1; j < 17; ++j) {
		fputc(' ', fout);
		fputc(a[((16-j)/2 + step) % 9], fout);
		++j;
	}
	fputc('\n', fout);
}
fputc('\n', fout);

读取文件中的数独题目并存储:

while (!fin.eof()) {
	//读取题目
	int count = 1;					//空格的编号
	for (int i = 0; i < 9; ++i) {
		for (int j = 0; j < 9; ++j) {
			fin >> array[i][j];
			if (array[i][j] == 0) {
				space[count].row = i;
				space[count].col = j;
				space[count++].anum = (i / 3) * 3 + (j / 3);
			}
		}
	}
	num_space = count - 1;
	ok = false;
	Tryspace(1);
	fout << endl;
}

深搜:

	//逐个试探空格中的数字
	if (ok) {
		return;
	}
	if (now > num_space) {
		for (int i = 0; i < 9; ++i) {
			for (int j = 0; j < 8; ++j) {
				fout << array[i][j] << " ";
			}
			fout << array[i][8] << endl;
		}
		return;
	}
	int row = space[now].row;				//当前空格的所在行
	int col = space[now].col;				//当前空格的所在列
	int anum = space[now].anum;				//当前空格的所在方格数
	for (int i = 1; i <= 9; ++i) {
		bool flag = true;					//检测是否符合数独规则
		array[row][col] = i;						//试探该空格的值
		for (int j = 0; j < 9 && flag; ++j) {
			//检测该行是否出现重复的数字
			if (array[row][j] == i && j != col) {
				flag = false;
				break;
			}
		}
		for (int j = 0; j < 9 && flag; ++j) {
			//检测该列是否出现重复的数字
			if (array[j][col] == i && j != row) {
				flag = false;
				break;
			}
		}
		for (int j = (anum / 3) * 3; j <= (anum / 3) * 3 + 2 && flag; ++j) {
			for (int k = (anum % 3) * 3; k <= (anum % 3) * 3 + 2 && flag; ++k) {
				//检测该方格里是否出现重复的数字
				if (array[j][k] == i && (j != row || k != col)) {
					flag = false;
					break;
				}
			}
		}
		if (flag) {
			//向下一个空格试探
			Tryspace(now + 1);
		}
	}
	array[row][col] = 0;

 

总结:

通过这个数独项目的学习和制作,我学习了一些在完整软件制作过程中的一些基本技巧,学会了使用性能分析软件来指导自己对代码进行优化。而且,还借此机会学习了CSDN和GitHub的使用。帮助自己养成在开发前进行计划,安排和设计的习惯,为自己以后的软件开发打下了良好的基础。

 

你可能感兴趣的:(软件工程基础——个人项目(解数独))