第二次作业——个人项目实战

第二次作业——个人项目实战

Github项目地址
作业地址

项目相关要求

利用程序随机构造出N个已解答的数独棋盘 。

输入

数独棋盘题目个数N

输出

随机生成N个 不重复 的 已解答完毕的 数独棋盘,并输出到sudoku.txt中,输出格式见下输出示例。
在生成数独矩阵时,左上角的第一个数为:(学号后两位相加)% 9 + 1。例如学生A学号后2位是80,则该数字为(8+0)% 9 + 1 = 9,那么生成的数独棋盘应如下(x表示满足数独规则的任意数字):

输入示例

sudoku.exe -c 1

输出示例

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

PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 30 40
· Estimate · 估计这个任务需要多少时间 20 20
Development 开发 300 350
· Analysis · 需求分析 (包括学习新技术) 100 180
· Design Spec · 生成设计文档 30 60
· Design Review · 设计复审 (和同事审核设计文档) 0 0
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 10 15
· Design · 具体设计 30 30
· Coding · 具体编码 200 180
· Code Review · 代码复审 30 40
· Test · 测试(自我测试,修改代码,提交修改) 200 250
Reporting 报告 20 20
· Test Report · 测试报告 20 20
· Size Measurement · 计算工作量 10 20
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 30 25
合计 1030 1250

解题思路

解题心理历程按照时间顺序分以下几个阶段

  • 刚拿到题目,自然想到暴力解决,递归回溯,深度优先,很开心得写好了最初的核心算法代码。
  • 接下来对写txt、参数的使用不了解,于是开始百度材料,另外新建立了一个项目,在新建的项目上测验写txt和参数的传递,对写txt和参数的使用有了一定的了解后,进一步修改了代码,随便又花了些时间实现了一下随机的功能。
  • 虽然自己的代码运行能得到结果,但是当N=1000000的时候,运行起来就变得非常慢了。不得不上网找找资料,查查别人是怎么实现数独的,花了挺多时间去寻找更加有效率的算法,最后挑选了行行变换、列列变换(矩阵变换法)。
  • 先是推理了一下为什么矩阵变换法的原理,最后发现貌似如果第一个数字固定生成的个数会不够多呀。矩虽然阵变换法快,但数量有限,回溯慢,但生成的数量可以非常非常多。
  • 想来想去,最后决定把自己之前敲的回溯与矩阵变换结合一下。而且两种方法各自都能保证生成数独唯一性。两种方法的结合,控制一下,也便能保证整体生成数独的唯一性。

设计实现

一共2个类

  • class Program //包含主函数
  • public class Creator //生成数独

一共7个函数:

  • void Main(string[] args) //主函数用来接收参数,并判断参数是否符合要求
  • public Creator(int temp) //构造函数,用来初始化数据,并接收主函数传递的参数
  • private void Begin() //开始寻找数独,先矩阵变换得到数独,再判断数量是否满足需求,不满足调用递归回溯函数
  • private void FindFirst(int p) //寻找第一个小九宫格,先生成第一个小九宫格(9个数字)
  • private void FindAll() //根据第一个九宫格,进行行行变换,列列变换,生成完整的数独
  • private void Dfs(int p) //递归回溯,生成完整的数独
  • private void Write() //把数独写入到txt文件

代码说明

快速生成,矩阵变换

生成第一个小九宫格:

private char[,] a = new char[9, 9];             //二维数组,用来存放整个数独的每一个数
private List<char> mylist = new List<char>();   //字符型List在函数begin()中初始化,使list包含字符1、2、3、5、6、7、8、9 (4为每个数独的首个,无需在List中)
private void FindFirst(int p)                    //寻找第一个小九宫格
{
	//采用递归回溯生成首个小九宫格
	int x, y, i, j, rand;
	if (count == over) return;
	if (p == 10)
	{
		FindAll();                          //成功生成首个小九宫格,调用FindAll()生成完整的数独
		return;
	}
	x = (p - 1) % 3;
	y = (p - 1) / 3;
	rand = re.Next(1, 100);                 //随机生成一个数字为下面循环做准备
	for (j = 0; j < mylist.Count(); j++)
	{
		i = (j + rand) % mylist.Count();    //使得首次循环i不一定都是从1开始循环,可以从2到最后一个中的某个数字开始循环,如循环次序:i= 5,6,7,8,1,2,3,4  实现一定的随机性
		a[x, y] = mylist[i];
		mylist.RemoveAt(i);                  
		FindFirst(p + 1);
		mylist.Insert(i, a[x, y]);
	}
}

矩阵变化的方法原理,参照这位同学,但是值得一提的是,每次生成下个小九宫格的时候有两种情况:

private void FindAll()                           //找到剩下的8个九宫格
{
	//采用行行变换,列列变换
	int i,q,w,e,r;
	if (count == over) return;
	for (q = 0; q < 2; q++)  //两种情况
	{
		//第2、3个九宫格变换
		for (i = 0; i < 3; i++)
		{
			...//代码类似第6、9个九宫格的变换,此处不再给出代码
		}
		for (w = 0; w < 2; w++)  //两种情况
		{
			//第4、7个九宫格变换
			for (i = 0; i < 3; i++)
			{
				...//代码类似第6、9个九宫格的变换,此处不再给出代码
			}
			for (e = 0; e < 2; e++)  //两种情况
			{
				//第5、8个九宫格变换
				for (i = 0; i < 3; i++)
				{
					...//代码类似第6、9个九宫格的变换,此处不再给出代码
				}
				for (r = 0; r < 2; r++)  //两种情况
				{
					//第6、9个九宫格变换
					for (i = 0; i < 3; i++)
					{
						a[X3 + 0, Y2 + i] = a[X3 + ((r == 0) ? 2 : 1), Y1 + i];         //6
						a[X3 + 1, Y2 + i] = a[X3 + ((r == 0) ? 0 : 2), Y1 + i];         //6
						a[X3 + 2, Y2 + i] = a[X3 + ((r == 0) ? 1 : 0), Y1 + i];         //6
						a[X3 + 0, Y3 + i] = a[X3 + ((r == 0) ? 1 : 2), Y1 + i];         //9
						a[X3 + 1, Y3 + i] = a[X3 + ((r == 0) ? 2 : 0), Y1 + i];         //9
						a[X3 + 2, Y3 + i] = a[X3 + ((r == 0) ? 0 : 1), Y1 + i];         //9
					}
					if (count == over) return;
					if (count < FAST_MAX) Write();  //理论上有645120个数独,为了使用回溯时避免重复,最后一组16个数独不用
					if (count == over) return;
				}
				if (count == over) return;
			}
			if (count == over) return;
		}
		if (count == over) return;
	}
}

继续生成,递归回溯

我们知道,如果利用矩阵变换,一共可以生成645120(8!x2x2x2x2)个数独,每16个的第一个小九宫格是一样的,为了避免重复,我们将最后的16个不在矩阵变换算法中写txt,我们利用最后这一组数独来生成递归,在递归中写txt,这样生成的数独剧不会重复了。

private void Dfs(int p)                         //回溯寻找数独
{
	int x, y, i, j, temp,rand;
	if (p == 1 || p == 2 || p == 3 || p == 10 || p == 11 || p == 12 || p == 19 || p == 20 || p == 21)         //防止破坏第一个小九宫格
	{
		Dfs(p + 1);
		return;
	}
	if (count == over)  return;                 //寻找到足够的数独,return
	if (p == 82)                                //找到一个数独,写txt 
	{
		Write();
		return;
	}
	x = (p - 1) % 9;
	y = (p - 1) / 9;
	rand = re.Next(1, 100);
	for (j = 1; j < 10; j++)
	{
		//其中flag_hang、flag_lie、flag_jiu是用来记录某行、列、九宫格出现数字,检测是否i符合要求
		i = (rand + j) % 9 + 1;
		if (flag_hang[y, i] == 1) continue;     
		if (flag_lie[x, i] == 1) continue;
		temp = (x / 3) + (y / 3) * 3;
		if (flag_jiu[temp, i] == 1) continue;
		else
		{
			a[x, y] = (char)i;
			a[x, y] += '0';
			flag_lie[x, i] = 1;
			flag_hang[y, i] = 1;
			flag_jiu[temp, i] = 1;
			Dfs(p + 1);                   //i放在p处,满足要求,寻找p+1
			flag_lie[x, i] = 0;           //回溯
			flag_hang[y, i] = 0;
			flag_jiu[temp, i] = 0;
		}
	}
}

测试运行

运行测试结果如下截图:


性能分析

改进前

从性能分析的截图上可以看出,Wirte的效率太低下了,花了好长的时间。

改进后

从朋友口中得知,原来使用stream的时候输出字符类型会快很多,于是对代码做了大幅度的更改,将byte型的数组,改为了char型的数组,果然效果很明显。

感想

  • 想和做差距是很大的,大家都很有想法,查完资料后更有想法,但是正真要做,总是会遇到的很多困难,泛泛而谈大部分人都会,我们不能养成那种说比做的还好听的坏习惯,有想法很好,但是同时也要有执行力。我们要去实现自己的idea,这样才能提高我们得执行力。idea只有变成了显示,它的价值才真正得被体现。
  • 在整个过程中还是遇到一些困难的:
    • 在每次修改完代码后,总是得找bug,找得很累呀。有时候经常找不到bug,这种时候,只能通过调试,写一些控制台输出变量来找bug。
    • 单元测试遇到的麻烦真心大,不懂得怎么写单元测试的代码,翻看了一些网上的资料,最后勉强写了一点测试自己的程序。
posted @ 2017-09-10 20:05  pengpeng_123  阅读(545)  评论(4编辑  收藏  举报