GitHub:sudoku
解题思路描述
刚看到题目的时候,我去,好难。吃了根冰棍冷静下来,开始细细思考。题目的要求是随机生成N个不重复的数独棋盘,有两种方案:1.用数字1~9填满第一个九宫格,然后再去填下一个九宫格,直到九个九宫格都填满,并且不会每一行每一列不会有相同的数字。2.用数字1填第一个九宫格,然后再填第二个.......直到九个九宫格都填入了1,再把数字变成2,再一个个地去填九宫格,以此类推,直到9个数字都填入。我认为第2种方案实现起来会更容易一些,因此决定采用第二种方案。
接下来便是确定数据结构和方法了。如果单纯采用二维数组来实现递归的话,我不知道该如何表示已经遍历过的格子。因此我决定采用链表加上二维数组的方式来实现数独棋盘的生成。
具体的方法如下:每个九宫格做一个含有九个结点的链表,Grid[g]存储第g个九宫格链表的首结点,blocks[g]用来表示第g个九宫格中空闲位置的数量(不包括已经尝试过的结点),递归函数PutNum和GetRandomValue互相配合往合适位置填入数字,当一个九宫格中无法填入数字,则向上一个宫返回false,如果可以则继续往下一个九宫格填数字。如果每次都是用这种递归方式随机生成数独棋盘,这样子效率太低,于是我想到一个方法,每随机生成一个数独棋盘之后,可以调换数字,这样就又成了一个新的数独棋盘,考虑到左上角的数字是固定的,所以这种换数字大法可以在一个随机数独棋盘的基础上生成40320种不同的棋盘。要实现这种换数字大法,就必须获取每种全排列的顺序,我使用permutation函数来生成全排列并将获取到的数据填入arr2中。
设计实现
代码中的全局变量弄得有点多,虽然我知道这样不好,但是不这样弄得话又感觉很不方便。代码中共有6个函数:BuildLinkedList()是用来建立存储坐标的链表;GetRandomValue(short g)是用来在第g个九宫格中放置数字num,当没有合法位置放置时,返回false;PutNum(short g)是用来递归调用的函数,该函数调用GetRandomValue来放置数字num;ShowSudoku()是用来将数独棋盘输出到文本文件中的函数;Permutation(short length)是用来产生全排列数组的函数;Clean()函数是当无法生成数独棋盘的时候,对一些动态变量进行清理,防止内存泄漏。
代码说明
这个是用来在九宫格中随机选取空闲可用位置的函数
bool GetRandomValue(short g)//在第g个九宫格中随机选取可用的位置来放入数字
{
if (blocks[g] == 0)return false;
int value;
value = rand() % blocks[g];//生成随机数
int i;
Node *p1, *p2;
for (i = 0, p2 = Grid[g], p1 = p2; i < 2 * blocks[g] - 1; i++)//p2即为可放置数字的位置坐标
{
if (i >= value&&row_flag[p2->row] == false && column_flag[p2->column] == false)break;
if (i == blocks[g] - 1)p2 = Grid[g], p1 = p2;
else
{
p1 = p2;
p2 = p2->next;
}
}
if (i == 2 * blocks[g] - 1)return false;
sudoku[p2->row][p2->column] = num;//接下来的代码是对被选中的目标位置结点进行删除前的准备操作
numlocation[g] = 3 * (p2->row % 3) + (p2->column % 3);
row_flag[p2->row] = true;
column_flag[p2->column] = true;
if (p2 == Grid[g])Grid[g] = p2->next;
if (p2 == LinkedListTail[g])LinkedListTail[g] = p1, p1->next = NULL;
if (p2 != Grid[g] && p2 != LinkedListTail[g])p1->next = p2->next;
delete p2;
blocks[g] = blocks[g] - 1;
return true;
}
调用GetRandomValue函数在每个宫内放置数字的函数PutNum
bool PutNum(short g)//在每个九宫格中放入相应的数字num
{
for (;;)
{
if (GetRandomValue(g) == true)
{
if (g == 8)return true;
else if (PutNum(g + 1) == false)//如果PutNum(g+1)返回false,则说明第g+1个宫无法放置数字,则在第g个宫尝试可以放置的其他位置,已经尝试过的位置结点则放置到链表后面
{
Node *p = new Node;
p->row = 3 * (g / 3) + numlocation[g] / 3;
p->column = 3 * (g % 3) + numlocation[g] % 3;
p->next = NULL;
LinkedListTail[g]->next = p;
LinkedListTail[g] = p;
sudoku[p->row][p->column] = 0;
row_flag[p->row] = false;
column_flag[p->column] = false;
continue;
}
else
{
blocks[g] = 9 - num;
return true;
}
}
else
{
blocks[g] = 10 - num;
return false;
}
}
}
生成全排列并存放在arr2数组中的函数Permutation
void Permutation(short length)//用递归的方法在arr2数组中生成全排列
{
int i;
if (length == 10 - sudoku[0][0])
{
if (length != 1)Permutation(length - 1);
else
ShowSudoku();
return;
}
for (i = 0; i<9 && stop_flag; i++)
{
if (arr1[i] == 0)
{
arr1[i] = 1;
arr2[9 - length] = i + 1;
if (length != 1)Permutation(length - 1);
else
ShowSudoku();
arr1[i] = 0;
}
}
}
将数独棋盘输出到文本文件中
void ShowSudoku()//将数独棋盘输出到文本文件中
{
int row, column;
for (row = 0; row < 9; row++)
{
for (column = 0; column < 8; column++)
{
fcout << arr2[sudoku[row][column] - 1] << " ";//将随机生成的数独棋盘映射到arr2数组中
}
fcout << arr2[sudoku[row][column] - 1] << endl;
}
if (--sudoku_count == 0)stop_flag = false;
else
fcout << endl;
}
测试运行
测试运行的截图
效能分析与改进
分析时生成的数独棋盘个数设为50000个
使用链表来进行生成数独棋盘真的很费时,效率不高,而且又占用空间。如果时间允许的话,我想不用链表来做,但是不用链表如何表示已经遍历过的位置对我来说是个问题,写完这篇博客准备去研究一下。
PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 720 | 1200 |
· Estimate | · 估计这个任务需要多少时间 | 720 | 1200 |
Development | 开发 | 660 | 1020 |
· Analysis | · 需求分析 (包括学习新技术) | 120 | 120 |
· Design Spec | · 生成设计文档 | 0 | 0 |
· Design Review | · 设计复审 (和同事审核设计文档) | 0 | 0 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 30 | 30 |
· Design | · 具体设计 | 60 | 180 |
· Coding | · 具体编码 | 360 | 600 |
· Code Review | · 代码复审 | 30 | 30 |
· Test | · 测试(自我测试,修改代码,提交修改) | 60 | 60 |
Reporting | 报告 | 60 | 180 |
· Test Report | · 测试报告 | 0 | 0 |
· Size Measurement | · 计算工作量 | 30 | 60 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 120 |
合计 | 720 | 1200 |
个人总结
第二次作业对我个人来讲难度还是很大的,这次代码用的数据结构也不是很好,处理链表带来的时间和空间开销都比较大。生成一个数独棋盘后替换数字又生成了另外一个棋盘是一个比较取巧的办法,时间开销比递归生成数独棋盘所用的时间要小,因此我采用了递归生成数独和换数字相结合的方式。还有就是这个完成这个作业的耗时远远在我的意料之外,花的时间实在是太长了,主要是前期规划不怎么好,导致编码的时候bug一大堆,很容易就停止运行了,也算是吸取一个教训了。最后一个就是自己的算法功底太薄弱了,代码也写的比较臃肿,新的学期要好好学习算法和代码的优化方法。
更新
得老师指点,将编译模式改为RELEASE模式,时间损耗是原来的四分之一,速度得到极大提升,因此对“效能分析与改进”板块进行修改,用RELEASE版的截图覆盖了原来的DEBUG版截图,并将RELEASE版的程序更新到GitHub上。
忽然发现如果将srand(time(0))放到递归函数中产生的随机数在短时间内会相等,而采用clock()函数做种子又会造成运行两次生成的2个文本文件有一定概率相等,然后尝试把srand(time(0))放到主函数中只调用一次,rand()函数放到递归函数中调用则产生的随机数即使在短时间内也不会相等,因此修改了随机数产生的代码,并将新的cpp和exe文件上传到GitHub上。