一、Github地址
Github地址:https://github.com/ZhangWuren/SoftwareEngineering
二、PSP表格
PSP | 过程 | 预估耗时 (分钟) | 实际耗时 (分钟) |
Planning | 计划 | 10 | 5 |
·Esitimate | ·估计耗时 | 10 | 5 |
Development | 开发 | 1020 | 870 |
·Analysis | ·需求分析 | 30 | 20 |
·Design Spec | ·生成设计文档 | 120 | 30 |
·Design Review | ·设计复审 | 30 | 10 |
·Coding Standard | ·代码规范 | 30 | 20 |
·Design | ·具体设计 | 150 | 100 |
·Coding | ·编码 | 300 | 300 |
·Code Review | ·代码复审 | 60 | 30 |
·Test | ·测试 | 300 | 360 |
Reporting | 报告 | 130 | 100 |
·Test Repor | ·测试报告 | 60 | 60 |
·Size Measurement | ·计算工作量 | 10 | 10 |
·Postmortem & Process Improvement Plan | ·总结,改进计划 | 60 | 30 |
总计 | 1160 | 975 |
三、解题思路
数独是我比较熟悉的一个概念。
1. 刚看到题目后,我首先思考的是生成数独的部分,一开始我的想法是DFS,从格(1,2)开始(因为第一行第一个数确定为学号末尾两位相加模9+1)随机选择1~9中的一个数,然后用深搜一次向下排列,判断的方法则是数独规则。但是思考了一会之后,就否定了这个算法,先不谈耗费时间过长的问题,因为是要生成上万数量级的数独,对生成的不重复性无法保证。后来在网络上搜索了一番,找到了一个更普遍的算法:通过对数独第一行的平移来生成数独。
比如,假定第一行为 123456789,将其向右平移3格,得到第二行789123456;将第一行向右平移6格,得到第三行456789123。显然,因为错位的关系,第一、二、三宫都符合数独规则。同理将第一行分别平移1,4,7格可以得到第四五六行,平移2,5,8格可以得到七八九行。因为第一行第一个数为6,剩余的全排列共8!=40320种,离题目的最大要求还有所差距。这时候只需要将四五六行全排列,七八九行全排列,即可生成 8!*3!*3! = 1451520>1000000种不同的数独。
2. 解数独方面,是一个典型的DFS+回溯法的问题,因为以前做过类似的C语言题目,所以就采用了C语言方法写。
四、设计实现过程。
在我的项目里,生成数独和解数独相对独立的两个的部分(甚至分别是C++和C语言实现的)
1.生成数独
在生成数独的部分,用到了一个CRaw类
class CRow
{ //数独的一行
public:
CRow()//构造函数将第一行设置为6 1 2 3 4 5 7 8 9
{
__row[0] = 6;
__trow[0] = 6;
for (int i = 1; i < 9; i++)
{
if (i < 6)
{
__row[i] = i;
__trow[i] = i;
}
else
{
__row[i] = i + 1;
__trow[i] = i + 1;
}
}
};
void NextRow();//对__row[] 进行一次排列
void TranslateAndPrintRow(int transNumber);//将__row[] 平移transNumber格 //结果赋值给__trow[] 并输出到文件
private:
unsigned short __row[9];//初始行,平移的模板
unsigned short __trow[9];//平移后的行
};
成员函数NextRow()是用c++标准库中函数next_permutation来实现全排列,例如将1 2 3→1 3 2→2 1 3
TranslateAndPrintRow(int transNumber) 根据参数transNumber来平移行并输出。
2.解数独
解数独是用回溯算法,主体函数为递归函数int search(int sudoku[][9], int order, int number)
int search(int sudoku[][9], int order, int number)
{
int copy[9][9];
int *copyPoint = (int *)copy;
int x = order / 9;
int y = order % 9;
for (int i = 0; i < 9; i++)
{
for (int j = 0; j < 9; j++)
{
copy[i][j] = sudoku[i][j];
}
}
if (order < 0)
searchFlag = 0;
if (order >= 0)
{
copy[x][y] = number;
}
if (searchFlag)
{
return 0;
}
while (*(copyPoint + order))
{
order++;
if (order > 80)
{
searchFlag = 1;
PrintSokudu(copy);
return 0;
}
}
for (int i = 1; i <= 9; i++)
{
x = order / 9;
y = order % 9;
if (isOK(copy, x, y, i))
{
search(copy, order, i);
}
}
return -1;
}
递归结束的标志是searchFlag=1,即下一个0的位置已经超过数独总格数81
调用了PrintSodoku()用来打印,isOK()用来判断合法性。
五、性能改进
在项目过程中,解数独的性能较为良好,需要改进的是生成数独的部分,断断续续花了大约一个星期左右的时间。
题目中输入的范围是1~1000000,在最开始的版本完成后,我尝试输出1000000个数独,结果时间却在6分钟左右。我开始寻找问题,一开始我以为是我的算法复杂度过高,但是我的计算显示我的算法复杂度应该是O(n)的,因为对于输入规模来说,我的解法是一个顺序的过程,不存在对输入规模n进行循环的地方,但是时间确实过长。
在我百思不得其解的时候,听取了同学的建议,尝试了一个新的算法,用空间换时间,将每个排列的36种模板直接用abcdefghi出来,之后只需要用对应的数字替代字母即可。但是结果仍然不如人愿,也需要5分钟左右。
后来在询问过别的同学之后,我发现是c++自带的fstream函数在文件读写的过程中耗费了大量的时间,在改用c语言FILE输出后,1000000个输出时间在5~9秒之间。
总结:我应该早点使用vs2017自带的性能分析工具!!!
下图是最终版本生成1000000个不重复数独的时间分析
可以看到,耗费时间的最多的地方是生成函数GenerateSudoku,其中主要是调用的TranslateAndPrintRow函数,这仍然还是输出导致的时间问题,实际上如果不需要输出,单纯生成的时间只需约0.8秒左右。
bool GenerateSudoku(char *csudokuNumber)
{
fp = fopen("sudoku.txt", "w");//打开sudoku.txt
//判断输入合法性
for (int i = 0; csudokuNumber[i] != '\0'; i++)
{
if (csudokuNumber[i] < '0' || csudokuNumber[i] > '9')//非数字
{
cout << "Please enter right number" << endl;
return false;
}
}
int sudokuNumber = atoi(csudokuNumber);
if (sudokuNumber > 1000000 || sudokuNumber <= 0)//不合法数字
{
cout << "Please input number between 0 and 1,000,000" << endl;
return false;
}
int count = sudokuNumber; //记录还未生成的数独个数
int TranslateArray1[3] = {1, 4, 7};//四五六行平移的格数
int TranslateArray2[3] = {2, 5, 8};//七八九行平移的格数
int TranslateArray3[3] = {0, 3, 6};//一二三行平移的格数
CRow crow;
for (int i = 0; i <= sudokuNumber / 36; i++)//每个全排列的行可以生成36个不同的数独
{ //故需要循环sudokuNumber/36次
int Times = 0; //Times用来判断对应全排列的行需要生成多少个不同的数独
if (i < sudokuNumber / 36) //除了最后一次循环未sudokuNumber%36次,别的循环都为36次
{
Times = 36;
}
else
{
Times = sudokuNumber % 36;
}
for (int j = 0; j < Times; j++)
{
count--;//未生成数独个数-1
for (int k = 0; k < 3; k++)//生成一二三行
{
crow.TranslateAndPrintRow(TranslateArray3[k]);
fputc('\n', fp);
}
for (int k = 0; k < 3; k++)//生成四五六行
{
crow.TranslateAndPrintRow(TranslateArray1[k]);
fputc('\n', fp);
}
for (int k = 0; k < 3; k++)//生成七八九行
{
if (k != 2)
{
crow.TranslateAndPrintRow(TranslateArray2[k]);
fputc('\n', fp);
}
else
{
crow.TranslateAndPrintRow(TranslateArray2[k]);
}
}
//变换 四五六 、 六七八行
if (j % 6 == 5)
{
next_permutation(TranslateArray1, TranslateArray1 + 3);
}
next_permutation(TranslateArray2, TranslateArray2 + 3);
if (count != 0)
{
fputc('\n', fp);
fputc('\n', fp);
}
}
crow.NextRow();
}
cout << "Already Generate right sudoku in sudoku.txt" << endl;
return true;
}
六、代码说明。
1.生成数独
生成数独部分,关键类为CRow,关键函数为GenerateSudoku()
class CRow
{ //数独的一行
public:
CRow()//构造函数将第一行设置为6 1 2 3 4 5 7 8 9
{
__row[0] = 6;
__trow[0] = 6;
for (int i = 1; i < 9; i++)
{
if (i < 6)
{
__row[i] = i;
__trow[i] = i;
}
else
{
__row[i] = i + 1;
__trow[i] = i + 1;
}
}
};
void NextRow();//对__row[] 进行一次排列
void TranslateAndPrintRow(int transNumber);//将__row[] 平移transNumber格 //结果赋值给__trow[] 并输出到文件
private:
unsigned short __row[9];//初始行,平移的模板
unsigned short __trow[9];//平移后的行
};
bool GenerateSudoku(char *csudokuNumber)
{
fp = fopen("sudoku.txt", "w");//打开sudoku.txt
//判断输入合法性
for (int i = 0; csudokuNumber[i] != '\0'; i++)
{
if (csudokuNumber[i] < '0' || csudokuNumber[i] > '9')//非数字
{
cout << "Please enter right number" << endl;
return false;
}
}
int sudokuNumber = atoi(csudokuNumber);
if (sudokuNumber > 1000000 || sudokuNumber <= 0)//不合法数字
{
cout << "Please input number between 0 and 1,000,000" << endl;
return false;
}
int count = sudokuNumber; //记录还未生成的数独个数
int TranslateArray1[3] = {1, 4, 7};//四五六行平移的格数
int TranslateArray2[3] = {2, 5, 8};//七八九行平移的格数
int TranslateArray3[3] = {0, 3, 6};//一二三行平移的格数
CRow crow;
for (int i = 0; i <= sudokuNumber / 36; i++)//每个全排列的行可以生成36个不同的数独
{ //故需要循环sudokuNumber/36次
int Times = 0; //Times用来判断对应全排列的行需要生成多少个不同的数独
if (i < sudokuNumber / 36) //除了最后一次循环未sudokuNumber%36次,别的循环都为36次
{
Times = 36;
}
else
{
Times = sudokuNumber % 36;
}
for (int j = 0; j < Times; j++)
{
count--;//未生成数独个数-1
for (int k = 0; k < 3; k++)//生成一二三行
{
crow.TranslateAndPrintRow(TranslateArray3[k]);
fputc('\n', fp);
}
for (int k = 0; k < 3; k++)//生成四五六行
{
crow.TranslateAndPrintRow(TranslateArray1[k]);
fputc('\n', fp);
}
for (int k = 0; k < 3; k++)//生成七八九行
{
if (k != 2)
{
crow.TranslateAndPrintRow(TranslateArray2[k]);
fputc('\n', fp);
}
else
{
crow.TranslateAndPrintRow(TranslateArray2[k]);
}
}
//变换 四五六 、 六七八行
if (j % 6 == 5)
{
next_permutation(TranslateArray1, TranslateArray1 + 3);
}
next_permutation(TranslateArray2, TranslateArray2 + 3);
if (count != 0)
{
fputc('\n', fp);
fputc('\n', fp);
}
}
crow.NextRow();
}
cout << "Already Generate right sudoku in sudoku.txt" << endl;
return true;
}
说明见注解
2.解数独
主要函数为递归函数search(),说明见注解
int search(int sudoku[][9], int order, int number)
{
//sudoku[][9]为递归的数组
//order为当前要设置的格子在数独中的顺序
//number为要对当前格子填下的数字
//递归函数
int csudoku[9][9]; //辅助数组,递归操作改变的是这个辅助数组,使得回溯时不需要再变动原数组,只需要将这个数组释放
int *csudokuPoint = (int *)csudoku;
int x = order / 9; //根据顺序转化为坐标
int y = order % 9;
for (int i = 0; i < 9; i++) //将递归的数组赋值给辅助数组
{
for (int j = 0; j < 9; j++)
{
csudoku[i][j] = sudoku[i][j];
}
}
if (order < 0) //初始化,开始递归
searchFlag = 0; //结束标志,0未结束,1结束
if (order >= 0) //将number赋值到csudoku中的对应位置
{
csudoku[x][y] = number;
}
if (searchFlag) //判断是否结束递归
{
return 0;
}
//搜索下一个为0的格子的顺序,即目标格
while (*(csudokuPoint + order) != 0)
{
order++;
if (order > 80)//如果顺序大于80,则表示所有格子都已经填满,设置searchFlag为1,打印结果
{
searchFlag = 1;
PrintSudoku(csudoku);
return 0;
}
}
for (int i = 1; i <= 9; i++)//对目标格填1~9
{
x = order / 9;//根据order获得目标格坐标
y = order % 9;
if (isOK(csudoku, x, y, i))//判断当前填法是否合法
{
search(csudoku, order, i);//如果合法,则递归目标格子
}
}
return -1;
}