项目Github地址 https://github.com/Tim-xiaofan/sudoku.git
PSP表格
准备与思考
系列博客
数独终局生成(2)----初步实现
数独终局生成(3)----完成第一个原型
数独终局生(4)----VS性能分析报告(已优化至4.646s左右)
说明博客贴出的代码不是最终代码,以项目中的代码为准。
-
Visual Studio GitHub代码托管配置
廖雪峰的网站有通俗易懂的Git教程 -
数独问题
-
命令行参数的传递
(1)控制终局生成的数量.例如sudoku.exe -c 20
涉及主函数的传参
(2)生成终局至文件sudoku.txt文件
##解题思路 -
关于输入参数的思考
主函数传参-c n;学号number;合法性判断
对于合法性判断应该有:(1)参数个数(2)是否为纯数字 -
数独终局的生成与输出
(1)限制:每个终局矩阵左上角已确定;
(2)满足数独规则
(3)采用什么算法生成
(4)如何输出 -
数独求解与输出
(1)如何判断(排除)有没有解
(2)如何求解
(3)如何输出
解题思路
数独终局的生成
-
通过第一行循环右移(注意左上角的数字已确定,这里为1)
//偏移值
int offset[9] = { 0,3,6,1,4,7,2,5,8 };
例如第一行为1 2 3 4 5 6 7 8 9,经过一次右移的第二行例如第一行为1 2 3 4 5 6 7 8 9,经过一次右移的第二行例如第一行为 7 8 9 1 2 3 4 5 6 。 剩余行依此类推,得到一个合法数独阵列
也就是说在这种生成规则下,第一行的不同组合数量就决定了中的阵列数量。由于左上角的数字1是固定的,故有8!= 40320种(4万)。而需求是10 ^6(100万),远远不够。考虑再次基础上继续变换 -
三行一组,组内任意两行互换(123)、(456)、(789)
例如上图图阵列, 交换8,9行位置后,如下图,依然合法。
在不考虑第一组的情况下,每种终局可以生成3!3!=36种全新的终局阵列。这样就总共有3640320=1451520>100w。
3.实现
(1)数据结构:先试试9乘9二维数组。
(2)不重复:这里理解为两点,一是某一次终局文件中没有重复的阵列,一是相同参数下不同时间终局文件不重复(可能有相同数独阵列)
(3)生成步骤:第一步,生成一个原始阵列;第二步,通过原始阵列进行变换,生成新的阵列;第三步,重复一、二步,直到满足数量为止。
(4)第一步的实现思路:**要生成一个原始阵列只需确定第一行,即从8!中取出一种情况。**但是如果8!中有一种情况已经使用过,就不能再使用了。为了避免这种情况,考虑从现有的排列生成新的排列。
查阅资料发现C++有相应的库,叫做STL库,库中实现了由当前排列求下一个排列的功能。
项目中只需简单的利用,参考博客https://blog.youkuaiyun.com/x_iaow_ei/article/details/28254413写了下面的demo
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 5;
const int len = 3;
int main()
{
int a[3]={3,4,5};
for(int i = 0; i < N; i++){
next_permutation(a, a+3); //下一个全排列,当没有下一个全排列时,函数返回值为0,
for(int i = 0; i < len; i++){ //并变为字典序最小的排列,否则函数返回值为1
cout << a[i];
}
cout <<" ";
}
return 0;
}
上面的代码输出了所有排列
关于下一个排列和上一个排列理解如下(摘自博客https://www.cnblogs.com/aiguona/p/7304945.html):
“下一个排列组合”和“上一个排列组合”,对序列 {a, b, c},每一个元素都比后面的小,按照字典序列,固定a之后,a比bc都小,c比b大,它的下一个序列即为{a, c, b},而{a, c, b}的上一个序列即为{a, b, c},同理可以推出所有的六个序列为:{a, b, c}、{a, c, b}、{b, a, c}、{b, c, a}、{c, a, b}、{c, b, a},其中{a, b, c}没有上一个元素,{c, b, a}没有下一个元素。
生成第一行的的问题解决了(如何生成和避免重复),那么接下来就是换行变换了**。考虑到在第一组(123行)不变换的情况下已经满足需求,这里只进行456和789两组的变换。记为A组,B组。
那怎么变换呢?
先考虑A组,总共有456 465 546 564 645 654。然后考虑B组,有789 798 879 897 978 987 。每确定一个A的排列,B组有对应的不同6中排列。如此反复,可以变换出36种。大致如下操作
for(int i = 0; i < 6; i++){
确定为A[i];
for(int j = 0; j < 6; j++){
确定为B[j];
保存或输出当前数独。
}
}
这样换行变换就解决了。
具体代码实现:
void newFromModel();方法
//在原始阵列基础上进行变换并保存36个排列
void SudokuFactory::newFromModel() {
//cout << "newFromModel()\n";
int A[3] = { 4, 5, 6 };//456为一组
int B[3] = { 7, 8, 9 };//789为一组
//前三行不变
char firstThreeRows[N * 6];// 保存123行
int index = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < N - 1; j++) {
firstThreeRows [index++]= (model[i][j]);
firstThreeRows[index++] = ' ';
}
firstThreeRows[index++] = (model[i][N - 1]);
firstThreeRows[index++] = '\n';//换行
}
//cout << sudokuString;
for (int a = 0; a < 6; a++) {
//确定A组456,的一个排列
//cout << "!!!!!!!!!!A:\n";
//printArray(A, 3);
char midThreeRows[6 * N];//保存456行
int index = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < N - 1; j++) {
//每生成一个数字,直接保存。注意空格和换行。
//cout << "(" << sudokuArray[A[index] - 1][j] << ")";
midThreeRows[index++] = (model[A[i] - 1][j]);
midThreeRows[index++] = ' ';
//cout <<"\n<"<< sudokuString << ">" << endl;
//cout << sudokuString;
}
//cout << "(" << sudokuArray[A[index] - 1][N - 1] << ")" << endl;
midThreeRows[index++] = (model[A[i] - 1][N - 1]);//行尾没有空格
//cout << "\n<" << sudokuString << ">" << endl;
midThreeRows[index++] = '\n';//换行
}
//cout << "transA:";
//cout << "\n<" << sudokuString << ">" << endl;
//同一个A的排列可以有6种不同的排列
for (int b = 0; b < 6; b++) {
//确定B组789,的一个排列
//cout << "B:\n";
//printArray(B, 3);
char lastThreeRows[6 * N];
int index = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < N - 1; j++) {
//每生成一个数字,直接保存。注意空格和换行。
//cout << "(" << sudokuArray[B[index] - 1][j] << ")";
lastThreeRows[index++] = (model[B[i] - 1][j]);
lastThreeRows[index++] = ' ';
}
//cout << "(" << sudokuArray[B[index] - 1][N - 1] << ")" << endl;
lastThreeRows[index++] = (model[B[i] - 1][N - 1]);//行尾没有空格
lastThreeRows[index++] = '\n';//换行
//cout << sudokuString;
}
//剩余需求量变化-1
need--;
//直接输出到文件
store(firstThreeRows,midThreeRows,lastThreeRows);
//cout << "need = " << need << endl;
//cout << "transB:";
//cout << oneSudokuString;
if (need == 0) return;//为零则,结束生成
//cout << "\n";//添加数独阵列间空行
sudokuStore[index_store++] = '\n';
//B的下一个排列
next_permutation(B, B + 3);
}
//A的下一个排列
next_permutation(A, A + 3);
}
}
代码简要说明:
其中N=9,二维数组sudokuArray保存当前的原始数独(模板数独),数组A,B分别记录456组和789组的变化情况,例如现在A[3]={4, 6, 5},即第五行和第六行进行了交换:
当我们要确定新的数独的第4行时,我们读取A[0],发现值为4,表明第4行与原始数独第4行一致,直接存入;当我们要确定新的数独的第5行时,我们读取A[0],发现值为6,表明第4行应该为模板数独的第6行数据,这是存入模板数独第六行即可。第六行同理。
789行也是同上操作。
当一个模板数独全部用完时,依然没有满足要求,我们就要生成新的模板数独,调用 上面的方法继续生成即重复**(a)生成模板数独(b)由模板数独进行变换**这两个步骤,知道满足需求为止。
补充说明,我们知道I/O操作是十分耗时的,为了减少I/O操作的次数,我们这里把结果先统一存入一个叫sudokuFileString的字符窜中,然后一次性输出到文件。
具体实现
1.参数的合法检测
由类ArgCheck的实现
类的定义如下:
class ArgCheck{
int argc;
char** argv;
int checkResult;//表明用户参数的合法与否,辨别用户想要做哪种操作
public:
//一些标志变量
static const int INVALID = 0;//参数不合法
static const int FORC = 1;//生成数独终局
static const int FORS = 2;//求解数独
public:
ArgCheck(int m_argc = 0, char** m_argv = NULL):checkResult(INVALID){
argc = m_argc;
argv = m_argv;
}
//提供checkResualt
int getResult() {
return checkResult;
}
//对参数的合法性以及参数进行判断,结果存入变量checkResualt并返回
int check();
};
其中只需调用int getResult()方法即可获参数的判别结果
2.生成数独
由类SudokuFactory实现
class SudokuFactory {
static const int N = 9;
int need;//剩余需求量
int index_store;
char* firstR;
char model[N][N];//模板
char* sudokuStore;//保存终局
public:
SudokuFactory(int m_need = 0 ) {
need = m_need;
index_store = 0;//j记录当前字符数量
sudokuStore = (char*)malloc(sizeof(char) * (18*N*need + need));
firstR = (char*)malloc(sizeof(int)*N);
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++)
model[i][j] = '0';
createFirstModel();//自动生成第一个模板
}
// 检查剩余需求量
bool isEnough() {
if (need > 0)return false;
return true;
}
//生成第一个模板
void createFirstModel();
//更新模板
void refreshModel();
//在原始阵列基础上进行变换并输出36个排列
void newFromModel();
//生成终局文件
char* createSudokuFile();//直接调用即可获得数独终局
string getsudokuStore() { return sudokuStore; }
void store(char a[], char b[], char c[]);
~SudokuFactory(){
free(sudokuStore);
}
};
其中只需调用char* createSudokuFile()方法即可获得数独终局
数独求解
解题思路
这里使用回溯的方法进行求解,大体思路如下:
在读入数独时记录相应的空格位置,宫,列,行的数字占用状态,以及当前的数独阵列。接着进行探索:
(1)检查当前空格是否还有数字可填,如果有,填入并更新宫,列,行的数字占用状态,以及当前的数独阵列,,同时注意是否是最后的空格,如果是表示已经找到了一个解。如果没有可填,返回上一个空继续探索。
(2)如果回到了第一个空格依然没有数字可填,表示无解。
具体实现
设计了类Rules记录占用宫,列,行的数字占用状态,并提供相应的更新方法
//列、行、宫
class Rules {
static const int N = 9;
bool* number;//数字填写情况,number[i] = true,表示(i + 1), i + 1 = (1, 2, ...,9)已填入
public :
Rules(){
number = new bool[N];
for (int i = 0; i < N; i++)
number[i] = false;
}
//判断数字num是否已经填入
bool is_added_number(char num);
//将数字num填入
void add_number(char number);
将数字num移除
void delete_number(char num);
void reset();
void print();
~Rules() { delete number; }
};
设计了类Space记录空格的可填数字,哪些已经用过
class Space {
int row, col, palace_index;//列号,行号,宫号。
int solution_count;//可行解个数
int used_count;//已经用过的可填数字
char solutions[9];//可行解集合
bool used[9];
char current_num;//当前填入的值
public:
Space(int m_row = 0, int m_col = 0) {
row = m_row;
col = m_col;
solution_count = 0;
used_count = 0;
current_num = '0';
palace_index = m_row / 3 * 3 + m_col / 3;
reset_used();
}
//判断当空格是否还有其它没有用过的可行解;
bool is_there_solutions();
//返回下一个可行解, 没有则返回‘0’
char next_solution();
//行号
int get_row() {
return row;
}
//列号
int get_col() {
return col;
}
//宫号
int get_palace() {
return palace_index;
}
//可行解个数
int get_solution_count() {
return solution_count;
}
void print();
void reset();
//全部重置:用过的可行解重新变为为没用过
void reset_used() {
for (int i = 0; i < 9; i++)
used[i] = false;
}
void set_used(int index) {
used[index] = true;
}
friend class SudokuSolve;
};
设计了类SudokuSolve.class使用者提供那个数独题目文件参数,调用solve()方法解出数独终局
class SudokuSolve {
static const int N = 9;
//数独限制条件
static Rules* rows;
static Rules* cols;
static Rules* palaces;
//空格区域
static Space* spaces;
int space_count;
//当前阵列情况
static char array[N][N];
//终局
char* sudoku_store;
//提取出一个数独谜题
bool get_puzzle();
//解决一个数独
bool puzzle_solve(int k);
//刷新行,列,宫
void rules_reset();
//判断数字num是否能填入
bool try_to_add(char mum, int space_index);
void print_array();
//更新占用状态
void refresh_rules(char num, int space_index, bool delete_num);
//回溯到某个有解节点时要先进行占用更新,再求解
//由于回溯到的节点已经填了某个数字,要先移除这个数字
void clear_space(int space_index);
//回溯经过的空格进行重置:用过的可行解重新变为为没用过
//同时,也要clear_space
void reset_space(int space_index) {
spaces[space_index].reset_used();
clear_space(space_index);
}
//初始化空格区域
void init_spaces();
//初始化静态成员
static void init_static();
//输出当前空格相关行,列,宫的占用情况
void print_space_status(int space_index);
public:
SudokuSolve();
char* solve();
};
相关测试
结果
优化
性能分析报告点击调试->性能探查器进行性能分析
1000时跑了1.043s
1w时跑了2.544s
10w时跑了15.433s,可以发现很慢了
100w时跑了2min19s,手机都刷了一圈了
分析100w的报告
点击报告中的main函数,发现调用的createSudokuFile函数占用时间最多98.32%
继续点击createSudokuFile函数,调用的newFromModel函数占用最多98.10%
继续点击newFromModel函数,发现to_string占用最多38.27%
发现问题
由于数组保存的int,再用运算符“+”拼接字符窜string结果是要转换成string,而使用to_string函数耗费了大量时间。现在考虑保存char,即把数组model与firstR都换成char型。改进后大大提升速度提升,100W耗时变成了2min19s变为48.721s,少了1min30s;可见to_string这个库函数还是避免使用.
当然,这比起宿舍大佬的6s还差得远呢
继续查看函数调用情况,发现字符窜的拼接占用了大量时间。思考是否摒弃字符窜保存终局
优化至4.646s左右
有点不敢相信,检查一下参数是100W没错的。在上面的分析报告中,我们看到字符窜的的处理速度太慢,所一我直接摒弃了字符窜,改用用字符数组保存终局,100W提升了近十倍。
PSP实际耗费时间
小结:
走了不少弯路,最开始没仔思考,用了字符窜保存结果,还用了效率低下的库函数to_string。
接着一步步优化,摒弃的int数组、库函数to_string,有了很大提升,最终摒弃了彻底摒弃了string,代码质量得到了巨大提升
数独终局生(2)