数独终局生成(1)-完整篇

项目Github地址 https://github.com/Tim-xiaofan/sudoku.git

PSP表格

在这里插入图片描述

准备与思考

系列博客
数独终局生成(2)----初步实现
数独终局生成(3)----完成第一个原型
数独终局生(4)----VS性能分析报告(已优化至4.646s左右)
说明博客贴出的代码不是最终代码,以项目中的代码为准。

  1. Visual Studio GitHub代码托管配置
    廖雪峰的网站有通俗易懂的Git教程

  2. 数独问题

  3. 命令行参数的传递
    (1)控制终局生成的数量.例如sudoku.exe -c 20
    涉及主函数的传参
    (2)生成终局至文件sudoku.txt文件
    ##解题思路

  4. 关于输入参数的思考
    主函数传参-c n;学号number;合法性判断
    对于合法性判断应该有:(1)参数个数(2)是否为纯数字

  5. 数独终局的生成与输出
    (1)限制:每个终局矩阵左上角已确定;
    (2)满足数独规则
    (3)采用什么算法生成
    (4)如何输出

  6. 数独求解与输出
    (1)如何判断(排除)有没有解
    (2)如何求解
    (3)如何输出

解题思路

数独终局的生成

  1. 通过第一行循环右移(注意左上角的数字已确定,这里为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万),远远不够。考虑再次基础上继续变换

  2. 三行一组,组内任意两行互换(123)、(456)、(789)
    例如上图图阵列, 交换8,9行位置后,如下图,依然合法。
    在这里插入图片描述
    在不考虑第一组的情况下,每种终局可以生成3!3!=36种全新的终局阵列。这样就总共有3640320=1451520>100w。
    3.实现
    (1)数据结构:先试试9乘9二维数组。
    (2)不重复:这里理解为两点,一是某一次终局文件中没有重复的阵列,一是相同参数下不同时间终局文件不重复(可能有相同数独阵列)
    (3)生成步骤:第一步,生成一个原始阵列;第二步,通过原始阵列进行变换,生成新的阵列;第三步,重复一、二步,直到满足数量为止。
    (4)第一步的实现思路:**要生成一个原始阵列只需确定第一行,即从8!中取出一种情况。**但是如果8!中有一种情况已经使用过,就不能再使用了。为了避免这种情况,考虑从现有的排列生成新的排列。
    查阅资料发现C++有相应的库,叫做STL库,库中实现了由当前排列求下一个排列的功能。
    项目中只需简单的利用,参考博客https://blog.csdn.net/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)

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值