Github项目地址
首先给出我的Github项目地址:https://github.com/XiaohanSnow/sudoku
模块耗费时间估计
然后估计在程序的各个模块的开发上耗费的时间
PSP | Personal Software Process Stages | 预估耗时 (分钟) | 实际耗时 分钟 |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 40 | |
Development | 开发 | ||
· Analysis | · 需求分析(包括学习新技术) | 90 | |
· Design Spec | · 生成设计文档 | 40 | |
· Design Review | · 设计复审(和同事审核 设计文档) | 15 | |
· Design Standard | · 代码规范(为目前的开发 制定合适的规范) | 15 | |
· Design | · 具体设计 | 90 | |
· Coding | · 具体编码 | 600 | |
· Code Review | · 代码复审 | 60 | |
· Test | · 测试(自我测试,修改 代码,提交修改) | 120 | |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 45 | |
· Size Measurement | · 计算工作量 | 45 | |
· Postmortem & Process Improvement Plan | · 事后总结,并提出过程 改进计划 | 45 | |
合计 | 1205 |
解题思路描述
生成终局
通过网上查询有关数独终局生成的资料,发现一种有规律的数独终局生成方法。任意生成数独的第一行,从第二行开始,每行依次是第一行右移3、6、1、4、7、2、5、8行,可以生成满足条件的数独终局。
下面给出一个生成数独终局的例子:
在数独第一行第一个数字选定为2的情况下,对第一行剩下八个数字进行全排列可以生成8!=40320种数独终局。对于这个数独终局,任意交换4 5 6行中的两行或是7 8 9行中的两行,都可以生成新的数独终局。这样就可以生成40320*3!*3!=1,451,520种数独终局,满足项目的要求。
求解数独
我参考了 暴力算法之美:如何在1毫秒内解决数独问题?| 暴力枚举法+深度优先搜索 POJ 2982中采用的解数独的方法,首先遍历数独,记录下空格的位置。利用三个数组分别记录每行、每列和每个九宫格中数字的出现情况。由于采用的是深度优先搜索,所以先填可填数字数目少的方格可以有效地减少函数递归调用自身的时间。方格可填数字的数目可以由方格所在行、列和九宫格已填的数字数目之和衡量,方格所在行、列和九宫格已填的数字越多,方格可填的数字越少。
实现过程
程序的主题是两个函数
void CreateSudoku(int & n);
void SolveSudoku(string path);
主程序根据输入的类型进行判断,
分别调用CreateSudoku函数或是SolveSudoku函数。
在CreateSudoku函数中,
使用c++的next_permutation函数获取数组全排列的下一组信息。
在SolveSudoku函数中,
使用自己定义的BlockNum函数根据空格的行、列计算所在九宫格的序号 ,
使用自己定义的SetMark函数记录方格有数字,
使用自己定义的Swap函数交换空格在宫格序列中的顺序,
使用自己定义的DFS函数进行深度优先搜索,
使用自己定义的Reset函数在深度搜索结束之后将计数的数组归零。
性能优化
首先考虑生成数独的情况。在最初的版本中,我采用的是逐行写入文件的方式,采用一个大小为17的数组存储一行的数字和空格,在写入一行后写入换行,每写入9行额外换行一次。这样不可避免地会产生数量巨大次数的打开文件关闭文件的时间消耗。
从减少I/O部分时间的目标出发,我决定用一个大小为200,000,000的数组存储数独的所有内容。一个数独的数字、空格、空行总数为163,这个数组可以存储1,000,000个数独的内容。最后一次性将数组写入文件,能够有效地减少写入文件的时间。
受到生成数独中算法的启发,在解数独的过程中,我同样采用了一次性读取和一次性写入的方法,使程序的性能有了明显的提升。
程序中消耗最大的函数为Write函数,将数组一次性写入文件。
性能优化后生成1,000,000个数独的时间为1.93s,效果明显。
代码说明
从初始化的满足学号开头的数组出发,全排列后九个数字,再依次替换数独中的数字,可以得到40320个数独终局。再对这些数独终局,交换4 5 6行和7 8 9行的数字,得到符合数目要求的数独终局
void CreateSudoku(int & n)
{
int FirstRow[] = {2,3,4,5,6,7,8,9,1}; //记录第一行的9个数
int RowOrder[] = {0,1,2,3,4,5,6,7,8}; //记录交换行以后行号的对应关系
int NumberOrder[9]; //记录9个数字在第一行全排列变换后的对应关系
int NewSudoku[9][9]; //记录第一行全排列变换后根据规则生成的新数独
int count = 0; //记录输出的个数
do
{
for (int i = 0; i < 9; i++) //9个数字在第一行全排列变换后与原位置数字一一对应
NumberOrder[Sudoku[0][i] - 49] = FirstRow[i];//原位置数字的ASCII码-48等于数字,再-1等于数组序号
for (int i = 0; i < 9; ++i) //将数独中的所有数字按照对应关系替换
for (int j = 0; j < 9; ++j)
NewSudoku[i][j] = NumberOrder[Sudoku[i][j] - 49];
for (int i = 0; i < 6 && n; i++)
{
for (int j = 0;j < 6 && n; j++)
{
for (int k = 0; k < 9; k++)
{
for (int t = 0; t < 9; t++)
{
Sudoku_out[count++] = NewSudoku[RowOrder[k]][t] +'0';
if (t == 8)
Sudoku_out[count++] = '\n'; //每行末尾换行
else
Sudoku_out[count++] = ' '; //方格之间空格
}
}
if (--n)
Sudoku_out[count++] = '\n'; //数独之间空行
else
return;
next_permutation(RowOrder+6,RowOrder+9); //对7 8 9行全排列变换
}
next_permutation(RowOrder+3,RowOrder+6); //对4 5 6行全排列变换
}
}
while(next_permutation(FirstRow+1,FirstRow+9)); //对第一行的后8个数全排列
return;
}
解决问题所要用到的数组
int unsolvedSudoku[10][10]; //记录待解决的数独
int rowMark[10][10]; //记录数字在行中是否出现
int colMark[10][10]; //记录数字在列中是否出现
int blockMark[10][10]; //记录数字在九宫格中是否出现
int row[10], col[10], block[10];//记录行、列、九宫格中出现的数字数目
int blank[100][3]; //记录空格所在的行、列、九宫格已经填入的数字数目
int blankCounter = 0; //记录空格的数目
记录方格是否有数字,将行、列、九宫格这个数字是否存在的flag置为1或0
inline void SetMark(int r, int c, int n, bool flag) //记录方格有数字
{
rowMark[r][n] = flag;
colMark[c][n] = flag;
blockMark[BlockNum(r, c)][n] = flag;
}
深度优先搜索,深度达到空格个数则停止搜索。如果方格所在行、列、九宫格均不存在此数字,则填入,向下一深度继续搜索。如果不满足条件,进行回溯,尝试下一个数字。
bool DFS(int deep) //深度优先搜索
{
if(deep==blankCounter) //求解完成
{
return true;
}
int r = blank[deep][0], c = blank[deep][1];
for(int i = 1; i < 10; i++)
{
if(!rowMark[r][i] && !colMark[c][i] && !blockMark[BlockNum(r, c)][i])
{
unsolvedSudoku[r][c]=i;
SetMark(r, c, unsolvedSudoku[r][c], 1); //空格填入数字
if(DFS(deep+1))return true;
SetMark(r, c, unsolvedSudoku[r][c], 0); //空格未填入数字
unsolvedSudoku[r][c]=0;
}
}
return false;
}
遍历数独,记录空格的行号和列号并记录空格数目。给有数字的方格打上标记,方格所在行、列、九宫格中有数字方格的个数加一。
while(Sudoku_in[count_in])
{
for(int r = 1; r < 10; r++)
{
for(int c = 1; c < 10; c++)
{
unsolvedSudoku[r][c] = Sudoku_in[count_in++] - 48;
if(unsolvedSudoku[r][c] == 0) //所在方格为空格
{
blank[blankCounter][0] = r;
blank[blankCounter][1] = c;
blankCounter++;
}
else //记录方格有数字
{
SetMark(r, c, unsolvedSudoku[r][c], 1);
row[r]++;
col[c]++;
block[BlockNum(r, c)]++;
}
}
}
GUI界面
输入数独题目,点击提交按钮后得到反馈。
时间统计
PSP | Personal Software Process Stages | 预估耗时 (分钟) | 实际耗时 分钟 |
---|---|---|---|
Planning | 计划 | ||
· Estimate | · 估计这个任务需要多少时间 | 40 | 30 |
Development | 开发 | ||
· Analysis | · 需求分析(包括学习新技术) | 90 | 80 |
· Design Spec | · 生成设计文档 | 40 | 50 |
· Design Review | · 设计复审(和同事审核 设计文档) | 15 | 20 |
· Design Standard | · 代码规范(为目前的开发 制定合适的规范) | 15 | 15 |
· Design | · 具体设计 | 90 | 120 |
· Coding | · 具体编码 | 600 | 500 |
· Code Review | · 代码复审 | 60 | 50 |
· Test | · 测试(自我测试,修改 代码,提交修改) | 120 | 100 |
Reporting | 报告 | ||
· Test Report | · 测试报告 | 45 | 50 |
· Size Measurement | · 计算工作量 | 45 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结,并提出过程 改进计划 | 45 | 40 |
合计 | 1205 | 1075 |