代码见GitHub
一、PSP
PSP | Process Stage | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planing | 计划 | 30 | 45 |
Estimate | 估计这个任务需要多长时间 | 1500 | 2500 |
Development | 开发 | 240 | 300 |
Analysis | 需求分析 | 60 | 60 |
Design Spec | 生成设计文档 | 30 | 20 |
Design Review | 设计复审 | 30 | 10 |
Coding Standard | 代码规范 | 20 | 20 |
Design | 具体设计 | 90 | 100 |
Coding | 具体编码 | 300 | 320 |
Coding Review | 代码复审 | 30 | 60 |
Test | 测试(自我测试、修改代码提交修改) | 120 | 90 |
Reporting | 报告 | 90 | 90 |
Test Report | 测试报告 | 20 | 20 |
Size Measurement | 计算工作量 | 20 | 20 |
Postmortem&Improvement | 事后总结及改进计划 | 30 | 30 |
二、基本思路
1. 终局生成
- 刚看到这个题目时的想法就是采用随机算法,每次产生一行1-9的随机数序列,然后将这一行序列放入到数独相应的行,判断当前行是否满足数独的要求。如果不满足,产生一个新的1-9随机数序列,再次进行判断;如果满足,进行下一行的生成,直到数独生成。但是这样生成数独非常慢,甚至无法生成数独。因为生成到后面几行的时候,随机生成的序列满足数独条件的可能性很小,因此需要不断生成随机序列,花费大量的时间。
- 第二个想法是改进的第一个想法。每次随机生成一个1-9的随机数序列作为数独中每一个格子的备选序列,每次只填一个格子。对于每一个格子,从备选序列中顺序的选择数字放到格子里,然后判断该格子在已填数字的格子中是否满足数独的三个条件。但是在生成过程中会遇到某个格子1-9均不能满足条件的情况,就必须回溯到上一个格子,从上一个格子的备选序列中选择一个未曾使用过备选数字填入,再判断,依次执行下去。由于用到了回溯的算法,所以实现过程中会用递归来实现。
项目要求产生的数独重复,所以使用了STL中的next_permutation()函数对第一行进行了全排列。
2.数独求解
- 在实现了数独生成的基础上进行数独求解就比较简单了。只需要记录每一个空位的位置(即行数和列数),然后逐个对每一个空位从1-9中选取一个数进行填数,判断当前位置所填的数是否可行。可行,则填下一个空位;若不行,则选取另一数进行当前位置的填数。若就个数都不可行,则回溯到上一个空位。
另外,为了程序的高效,可以对需要求解的数独进行预处理。即在求解之前先检查每一个空位,根据已有数字推算出每个空位可以填的数字,并将其保存下来。这样,在空位上填数的时候不用从1-9中选,只需在自己可以填的数字中选取数字就可以了。
三、实现过程
终局生成
在生成数独过程中,必不可少的就是要检查某个位置上所填的数是不是符合数独的三条规则,所以要有Check()函数,其代码如下:
bool Check(int Row, int Col)
{
int m = map[Row][Col];
for (int i = 0; i < Row; i++)
{
if (m == map[i][Col])
return false;
}
for (int i = 0; i < Col; i++)
{
if (m == map[Row][i])
return false;
}
int RowStart = Row / 3 * 3;
int ColStart = Col / 3 * 3;
int RowEnd = RowStart + 2;
int ColEnd = ColStart + 2;
for (int i = RowS; i <= Row; i++)
for (int j = ColS; j <= ColE; j++)
{
if (i >= Row && j >= Col)
break;
if (m == map[i][j])
return false;
}
return true;
}
其中前两个循环分别检查是否与所在列和所在行冲突,因为数独是从左上角开始生成,行与列只需检查到自身即可。第三个循环是检查当前位置所在的3x3的小块内是否有冲突。
在生成终局时需要回溯,利用递归实现回溯算法:
bool Generate(int Row, int Col)
{
int NextRow;
int NextCol;
stack<int> s;
int buffer[9];
for (int i = 0; i < 9; i++) {
buffer[i] = 1 + rand() % 9;
for (int j = 0; j < i; j++)
if (buffer[i] == buffer[j])
{
i--;
break;
}
}
for (int i = 0; i < 9; i++)
s.push(buffer[i]);
while (!s.empty())
{
map[Row][Col] = s.top();
s.pop();
if (!Check(Row, Col))
continue;
if (Col == 8)
{
if (Row == 8)
return true;
else {
NextCol = 0;
NextRow = Row + 1;
}
}
else
{
NextRow = Row;
NextCol = Col + 1;
}
bool Next = Generate(NextRow, NextCol);
if (Next) return true;
}
if (s.empty())
return false;
return true;
}
这里采用栈来存储每个位置的备选数字,当生成时回溯到当前位置,只需对当前栈进行pop()操作即可直接得到下一个备选数字。
采用递归的时候要注意的一点就是返回条件,不小心可能会使程序崩溃。
数独求解
开始解数独之前先根据已有数字进行预处理
void Pretreatment()
{
N = 0;
for (int i = 0; i < 9; i++)
{
for (int j = 0; j < 9; j++)
{
if (map[i][j] == 0)
{
address[N][0] = i;
address[N][1] = j;
N++;
}
}
}
for (int i = 0; i < N; i++)
{
for (int j = 0; j < 9; j++)
{
map[address[i][0]][address[i][1]] = j+1;
if (Check_2(address[i][0], address[i][1]))
inf[i][j] = true;
else inf[i][j] = false;
}
map[address[i][0]][address[i][1]] = 0;
}
}
其中,数组address记录每一个空位的位置信息(行数和列数),inf数组则用下标记录每一个空位可以放的数字.比如 inf[1][1] = true ,代表第二个空位可以放数字 “ 2 ”。
在数独求解部分同样采用回溯法,利用递归进行求解
bool Solving(int count)
{
if (count < 1)
return true;
int n = N - count;
for (int j = 0; j < 9; j++)
{
if (inf[n][j])
{
map[address[n][0]][address[n][1]] = j + 1;
if (Check(address[n][0], address[n][1]))
{
if(Solving(count-1))
return true;
}
}
}
map[address[n][0]][address[n][1]] = 0;
return false;
}
对于每一个空位,遍历其 inf 数组,若为真并且该数字可以放在当前位置则递归调用下一个空位。
四、总结
性能分析
- 生成
这是生成10000个数独时的情况,耗时4.836秒
- 求解
求解自己写的10个简单数独问题