**
问题概述:
**
任务:实现一个能够生成数独终局并且能求解数独问题的控制台程序。
1.生成不重复的数独终局至文件。
2.读取文件内的数独问题,求解并将结果输出到文件。
代码仓库地址:个人项目
基本思路:
拿到题目的时候其实没有看懂到底要求做什么,逮着室友问了之后终于了解了问题是什么。
整个项目分为两个部分,第一部分为生成数独,即为输入命令,创建一个txt文件,然后将代码生成的相应数量的数独按照一定格式写入这个txt文件。
第二部分为求解数独,即为输入命令,读取指定文件的数独问题(有挖空的数独),然后求解,再将得到的数独终局写入另一个txt文件中(与第一部分生成数独的文件相同,内容覆盖)。
生成数独终局部分
一开始想到用一个初始的数独终局,考虑标题左上角的数字固定,然后单个数字替换法(7+6+5+4+3+2+1=33种)加上行列交换(77=49种),但这样只能产生3349=1617种终局,远远不够题目要求的1~1e6种。如果用单纯的随机数生成,在判断是否重复上肯定又要花费大量时间。在网上找资料之后发现了另一种思路: 用一个1-9无重复的排列有规律地进行平移,可以产生一个完整数独终局。即用第一行即可用平移来生成其余的2-9行。因为要保证左上角数字固定,平移的间隔为3。首行第一个数字固定为9,向右分别平移3、6、1、4、7、2、5、8位的示例如下。
9 1 2 3 4 5 6 7 8
3 4 5 6 7 8 9 1 2
6 7 8 9 1 2 3 4 5
1 2 3 4 5 6 7 8 9
4 5 6 7 8 9 1 2 3
7 8 9 1 2 3 4 5 6
2 3 4 5 6 7 8 9 1
5 6 7 8 9 1 2 3 4
8 9 1 2 3 4 5 6 7
第一行可以认为平移了0位。前三行的第一行锁死不能移动,因此只有第二行和第三行可以进行一次交换,即为只有“036”和“063”两种平移方式。
其余456行和789行在内部各可以进行{“147”、“174”、“417”、“471”、“714”、“741”}和{“258”、“285”、“528”、“582”、“825”、“852”}的六种平移方式。平移方式还可以进行组合,所以共能组合出266=72种不同的方式。
每个无重复的1-9排列可以生成72种终局,而排列可以有8!=40320种,即总共可以产生72*8!=2903040种终局,满足1~1e6的数量范围要求。
重要变量
char Data[10] = { ‘1’,‘2’,‘3’,‘4’,‘5’,‘6’,‘7’,‘8’ }; :初始存放除了固定数字之外的八个数字,后来要将固定数字加到排列尾部。
char Sudoku[200000002];:一个极大的一维字符串数组,采用这种方法存储最终打印的数独数据能大幅降低系统运行时间,因为系统会将很多时间花在处理文件上。
重要函数
void BuildMap();:生成总体的数独图。
voidBuildSudoku(char rule1, char rule2, char* rule3);**:生成一个数独。
void ThreeCows(char rule);*:按照传递的规则生成三行排列。
求解数独终局部分
基本思想当然是回溯。网上查找资料后,解题思路如下:
重要变量声明
char Rule[3][10][10]:一个型三维数组,第一位为0时代表行,此时第二位代表行号,第三位代表1-9的某个数字;第一位为1时代表列,此时第二位代表列号;第三位为2时代表宫,此时第二位为宫号。通过Rule数组我们可以查找当前位置(空格)上某个数字是否符合填入规则(即在本行、本列、本宫,都没有出现过这个数字),
char State[9][9]:一个二维数组,代表了数独的当前分布状态。
重要函数
void ReadSudokuFile(int row,int string); :读取写有数独问题的文件。
void CheckRule(int row,int column,int num);:用于检查当前位置数字num是否符合填入规则。
void SetRule(int row,int column,int num,bool state);:用于标明当前位置数字num是否被占用。state为flase时表示未被占用,为true时表示已被占用。
void SolveSudoku(int row,int column);:求解数独的核心部分,用递归来试探填入数字寻求解,如果找不到就擦除搜索标记和一步步回到可以重新填入的地方。
**
遇到问题:
1.在命令行界面输入命令无效,如图所示。
原本以为是Sudoku文件有问题无法运行,折腾好久才想起可能是没有定位到exe文件所在目录。解决途径(win10):输入磁盘号定位到文件所在磁盘—>用cd定位到exe文件所在目录—>输入指定命令。
2.在生成首行排列时费了很多时间想去写循环,但发现单纯的循环来实现排列组合需要的参数太多,又复杂又容易出错。后来用了STL标准模板库的next_permutation函数来实现升序全排列,非常省事。其函数原型为
#include<algorithm>
bool next_permutation(iterator start,iterator end);
next_permutation(num,num+n)即为对数组num中的前n个元素进行全排列,同时改变num数组的值。
3.当数量很大时,程序不运行。
在测试的时候,指定数量为666666,命令行不输出我所要的答案。检查了NeedCount、NowCount等相关变量的类型,都为long long int,那么问题在哪儿呢?经过测验发现是我用来存储数独的数组开得不够大。之前用的是100000002,改为200000002后可以正常输出。
错误情况:
输入:Sudoku.exe -c 666666
输出:Great! Now we begin to create sudoku!
NeedCount:666666
正确情况:
输入:Sudoku.exe -c 1000000
输出:Great! Now we begin to create sudoku!
NeedCount:1000000
You use 2.731 seconds to build these sudoku!
更新ing
代码部分:
1.生成1-9的排列
void ThreeCrows(char* rule){
for(int i = 0; i < 3; i++){
Sudoku[datap++] = Data[(8 + rule[i] - '0') % 9];
for(int j = 1; j < 17; j++){
Sudoku[datap++] = ' ';//相邻数字之间的空格
j++;
Sudoku[datap++] = Data[((16 - j) / 2 + rule[i] - '0') % 9];//数组
}
Sudoku[datap++] = '\n';//每一行数字的换行
}
}
void BuildSudoku(char* rule1,char* rule2,char* rule3){
ThreeCrows(rule1);
ThreeCrows(rule2);
ThreeCrows(rule3);
Sudoku[datap++] = '\n';//数独之间的换行
}
2.生成总的数独终局
void BuildMap(){
char rule1[10][10] = {"036","063"};
char rule2[10][10] = {"147","174","417","471","714","741"};
char rule3[10][10] = {"258","285","528","582","825","852"};
do{
Data[8] = '9'; //插入固定数字到末尾,这里为(6+2)%9+1 = 9
for(int i = 0; i < 2; i++){
for(int j = 0; j < 6; j++){
for(int k = 0; k < 6; k++){
BuildSudoku(rule1[i],rule2[j],rule3[k]);
NowCount++;
if(NowCount == NeedCount){
return ;
}
}
}
}
}while(next_permutation(Data,Data + 8));//next_permutation函数使用前需要对排列进行升序排序 ,否则只能找出该排列之后的全排列
}
3.求解数独的递归函数
void SolveSudoku(int row, int column) {
bool IsSearch = false;
while (State[row][column] != '0') {//循环直到找到一个数独问题的空格
if (column < 8) {
column++; //列号小于8则列号+1,检查本行下一位数字
}
else {
column = 0;//否则将列号置零,行号+1,检查下一行
row++;
}
if (row == 9) {//如果循环查找完毕都没有找到空格,表明数独的解已被找到
IsFindAns = true;
return;
}
}
for (int i = 1; i <= 9; i++) {
if (CheckRule(row, column, i)) {//如果数字i在当前位置符合填入规则 ,将其填入此空格
SetRule(row, column, i, 1);//标明当前位置该数字 i 已被占用
State[row][column] = i + '0';//更新当前数独状态的数组
IsSearch = true;
SolveSudoku(row, column);//递归调用SolveSudoku函数 ,查找下一个空格位置可能的解
}
if (IsSearch) {//如果已被搜过,没有进入递归(没有找到可以填入的数字),那么开始回溯
IsSearch = false;
if (IsFindAns) {
return;
}
else {
State[row][column] = '0';
SetRule(row, column, i, 0);//将原本填入的数字擦掉,同时消掉该数字的占用标记
}
}
}
}
4.规则函数
void SetRule(int row, int column, int num, bool state) {
Rule[0][row][num] = state;
Rule[1][column][num] = state;
Rule[2][gong][num] = state;
}
bool CheckRule(int row, int column, int num) {
if (Rule[0][row][num] == 0 && Rule[1][column][num] == 0 && Rule[2][gong][num] == 0) {//如果在本行、本列、本宫,数字num都没有出现
return true;//那么可以填入
}
else {
return false;
}
}
5.最终的main函数
int main(int argc, char* argv[])
{
clock_t start;
clock_t finish;
double duration;
if (argc == 3 && strcmp(argv[1], "-c") == 0) {
for (int i = 0; i < strlen(argv[2]); i++) {
if (argv[2][i] >= '0' && argv[2][i] <= '9') {
NeedCount *= 10;
NeedCount += argv[2][i] - '0';
}
else {
cout << "Input error! Please enter the number in right format!" << endl;
return 0;
}
}
cout << "Great! Now we begin to create sudoku!" << endl;
cout << "Needcount:" << NeedCount << endl;
start = clock();
while (NowCount < NeedCount) {
BuildMap();
}
Sudoku[datap++] = '\n';//每个组数独之后的换行
WriteBuildSudoku();
finish = clock();
duration = (double)(finish - start) / CLOCKS_PER_SEC;
cout << "You use " << duration << " seconds to build these sudoku!" << endl;
}
else if (argc == 3 && strcmp(argv[1], "-s") == 0) {
cout << "Oh!Let's start to get these sudoku's solutions!" << endl;
FILE *fp1 = fopen(argv[2], "r");
char string[100];
int rowline = 0;
start = clock();
while (fgets(string, 20, fp1)) {
if (strcmp(string, "\n") == 0) {
continue;
}
ReadSudokuFile(rowline, string);
rowline++;
if (rowline == 9) {
if (!IsFirstSudoku) {//不是第一个数独问题则打印一个换行
OutputSolveFile << endl;
}
else {
IsFirstSudoku = false;
}
IsFindAns = false;
SolveSudoku(0,0);
rowline = 0;
WriteOneSudoku();
memset(Rule, 0, sizeof(Rule));//每次求解完一个数独后将Rule重置归零
}
}
finish = clock();
duration = (double)(finish - start) / CLOCKS_PER_SEC;
cout << "You use " << duration << " seconds to solve these sudoku!" << endl;
}
else {
cout << "Input error! Wrong format!" << endl;
}
system("pause");
return 0;
}
性能分析
VS2017的分析—>性能探查器—>性能向导—>检测—>一个或多个可用项目—>Sudoku—>完成。