第二次作业——个人项目实战
项目相关要求
利用程序随机构造出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。
- 单元测试遇到的麻烦真心大,不懂得怎么写单元测试的代码,翻看了一些网上的资料,最后勉强写了一点测试自己的程序。