C++数独求解器与生成器

前几天笔者外出培训,刚刚学习了深度优先搜索,突然想到了数独的求解其实也可以用深搜实现,遂写了数独求解器与生成器。

1 数独求解器

1.1 预备

一开始,当然是头文件~

#include 
#include 


接下来,是变量定义区

int sudoku[10][10];
//题目存储区
int row[10][10],column[10][10],block[4][4][10];
/*标记行、列、九宫格内某数字是否被使用过,例如row[4][7]=1代表第4行已有数字7*/
int known[10][10];
/*标记某单元格是否是题目所给数字,1代表题目中的数字*/
/*以上数组下标均从1开始使用*/

1.2 核心算法——深搜

相信学过深搜的朋友应该知道,深搜的过程含搜索与回溯两步。

void search(int x,int y){
	if (known[x][y]){
		search((9*x+y-9)/9+1,y%9+1);  //如果是题目中的数字则直接搜索下一个单元格,这里一并处理了y=9即现在所处单元格在行末的情况
	}
	else{
		if (x==10 && y==1) { //已经填完所有的数并合法
			print(); //打印输出
			exit(0); //找到一个解就退出。若欲找出所有的解,将此行去掉即可
		}
		else
		for (int i=1;i<=9;i++){  //枚举填数1~9
			if (row[x][i]==0 && column[y][i]==0 && block[(x-1)/3+1][(y-1)/3+1][i]==0){ //判断此数是否合法。九宫格计算略显复杂
				sudoku[x][y]=i;
				row[x][i]=1;
				column[y][i]=1;
				block[(x-1)/3+1][(y-1)/3+1][i]=1;
				search((9*x+y-9)/9+1,y%9+1);//填入下一个数,计算方法同第三行
				sudoku[x][y]=0;
				row[x][i]=0;
				column[y][i]=0;
				block[(x-1)/3+1][(y-1)/3+1][i]=0;//回溯:恢复之前的状态
			}
		}
	}
}


以上代码,一气呵成。

1.3 输出

为获得较好的视觉效果,笔者决定,输出格式用“-”和“|”画出单元格边界,用“=”和“||”表示九宫格边界和题目的边界。具体效果如下图所示:

C++数独求解器与生成器_第1张图片

简单。马上给出代码实现:

void print(){
	cout<<"    1   2   3    4   5   6    7   8   9   "<

当然笔者并未满足,整天对着黑白的控制台也会疯掉的。百度了一下C++如何使输入输出带上颜色之后,笔者改进了输出函数:

const int colour [10]={0,3,4,13,5,11,6,14,2,9};//为每个数字设置颜色
void print(){
	cout<<"    1   2   3    4   5   6    7   8   9   "<

以上代码需加上windows.h头文件。最终效果如下图:

C++数独求解器与生成器_第2张图片

1.4 初始化

万事俱备,最后的输入工作也慢慢完成了。且看代码:
int main(){
	for (int i=1;i<=9;i++){
		for (int j=1;j<=9;j++){
			cin>>sudoku[i][j];
			if (sudoku[i][j]) known[i][j]=1;//从这里可以看出,本程序用0代表空白。此处设置known数组,使程序分辨出是题目所给数字还是空白。
		}
	}
	for (int i=1;i<=9;i++){
		for (int j=1;j<=9;j++){
			if (sudoku[i][j]){
				row[i][sudoku[i][j]]=1;
				column[j][sudoku[i][j]]=1;
				block[(i-1)/3+1][(j-1)/3+1][sudoku[i][j]]=1;//构造候选数表
			}
		}
	}
	search(1,1);//从(1,1)开始搜索
	return 0;
}

也就是说,输入格式如下:
sample in
0 6 0 0 0 0 9 3 0
0 9 3 0 1 6 5 8 0
0 0 0 0 9 0 0 0 0
6 0 7 1 8 9 4 2 0
4 0 0 0 0 2 3 0 8
0 8 0 0 4 7 1 0 6
3 0 6 9 0 8 0 5 0
5 7 1 2 3 0 8 6 9
9 2 8 6 0 0 7 0 3
综上,数独求解器已经全部完成。完整代码如下:
#include 
#include 
#include 
using namespace std;

int sudoku[10][10];
int row[10][10],column[10][10],block[4][4][10];
int known[10][10];
const int colour [10]={0,3,4,13,5,11,6,14,2,9};
void print(){
	cout<<"    1   2   3    4   5   6    7   8   9   "<>sudoku[i][j];
			if (sudoku[i][j]) known[i][j]=1;
		}
	}
	for (int i=1;i<=9;i++){
		for (int j=1;j<=9;j++){
			if (sudoku[i][j]){
				row[i][sudoku[i][j]]=1;
				column[j][sudoku[i][j]]=1;
				block[(i-1)/3+1][(j-1)/3+1][sudoku[i][j]]=1;
			}
		}
	}
	search(1,1);
	return 0;
}

2 数独生成器

数独生成器要做的事情要稍多一些。第一,要随机填数。第二,要确保生成出来的数独有且仅有一组解(否则别人会卡在多解的地方无法继续推理填数)。笔者在数独求解器的基础上,对主程序和search函数进行改动,便写出了数独生成器。

2.1 search函数

随机填数,简单粗暴,不做解释。直接贴代码:
void search(int x,int y){
	if (f) return;
	if (known[x][y]){
		search((9*x+y-9)/9+1,y%9+1);
	}
	else{
		if (x==10 && y==1) {
			f=1;//生成完毕标识。可不能用exit(0)
			cnt++;
		}
		for (int j=1;j<40;j++){
			int i=rand()%9+1;//生成[1,9]随机整数
			if (row[x][i]==0 && column[y][i]==0 && block[(x-1)/3+1][(y-1)/3+1][i]==0){
				sudoku[x][y]=i;
				row[x][i]=1;
				column[y][i]=1;
				block[(x-1)/3+1][(y-1)/3+1][i]=1;
				search((9*x+y-9)/9+1,y%9+1);
				if (!f){
					sudoku[x][y]=0;
					row[x][i]=0;
					column[y][i]=0;
					block[(x-1)/3+1][(y-1)/3+1][i]=0;
				}
			}
		}
	}
}

2.2 main函数

由于此处解释过长,请原谅笔者将解释放到代码的注释中了。
int main(){
	srand(time(0)*time(0)-0x5e2d6aa*rand()+time(0)*338339);//只是想写一个复杂的种子值
	search(1,1);//生成一个数独
	int sum=0;
	int sudoku2[10][10]={0};//sudoku2用于保存原题
	for (int i=1;i<=9;i++){
		for (int j=1;j<=9;j++){
			problem[i][j]=sudoku2[i][j]=sudoku[i][j];
		}
	}
	cnt=0;
	while (cnt!=1){//cnt就是数独解的个数
		for (int i=1;i<=9;i++){
			for (int j=1;j<=9;j++){
				known[i][j]=1;//先假设所有数都是题目所给
			}
		}
		sum=0;
		while (sum<50){//50意为生成题目的空白数
			int x=rand()%9+1,y=rand()%9+1;
			if (known[x][y]==0) continue;
			else {
				sum++;
				known[x][y]=0;//随机设置空白
			}
		}
		for (int i=1;i<=9;i++){
			for (int j=1;j<=9;j++){
				if (known[i][j]==0) sudoku[i][j]=problem[i][j]=0; //将空白处置0
			}
		}
		cnt=0;
		f=0;
		memset(row,0,sizeof(row));
		memset(colom,0,sizeof(column));
		memset(block,0,sizeof(block));//全部清空候选数表
		init();//重新设置候选数表,具体实现见后面
		search2(1,1);//其实就是1.1里面的search函数,不过一并记录了数独解的个数
		for (int i=1;i<=9;i++){
			for (int j=1;j<=9;j++){
				sudoku[i][j]=sudoku2[i][j];//恢复原题
				if (cnt!=1) problem[i][j]=sudoku2[i][j];//若多解,problem也恢复原题。否则,problem就是我们的题目
			}
		}
	}
	print(problem);//print函数稍作更改,带了一个参数。具体实现见下
	return 0;
}

2.3 print函数

其实print函数只改动了一处:
void print(int sudoku[][10])
巧用C++中生存期的概念,让形参与全局变量同名,使得sudoku数组这一全局变量在print函数中不可见,而原print函数中调用sudoku数组的语句都是无需改动的。各位朋友可慢慢体会其妙处。

2.4 代码实现

说了这么多,也该贴上全部代码了:
#include 
#include 
#include 
#include 
#include 
using namespace std;

int sudoku[10][10];
int problem[10][10];
int row[10][10],column[10][10],block[4][4][10];
int known[10][10];
int f=0;
int cnt=0;
const int colour [10]={0,3,4,13,5,11,6,14,2,9};
void init(){
	for (int i=1;i<=9;i++){
		for (int j=1;j<=9;j++){
			if (sudoku[i][j]){
				row[i][sudoku[i][j]]=1;
				column[j][sudoku[i][j]]=1;
				block[(i-1)/3+1][(j-1)/3+1][sudoku[i][j]]=1;
			}
		}
	}
}
void print(int sudoku[][10]){
	cout<<"    1   2   3    4   5   6    7   8   9   "<
运行效果如下图:
C++数独求解器与生成器_第3张图片
读者朋友们,你们能解出这道来自C++的数独题吗?
warning:虽然主程序中空白块的数量可以任意改动,但笔者建议不要大于53,否则程序会长时间运行,很久才会出结果。

2.5 一个bug

运行数独生成器的时候,有时程序会像陷入死循环一般,无论等多长时间都不会动弹。笔者尚未找出此问题产生的原因及解决方案。若遇到此种情况,建议直接按Ctrl+C结束程序,再次启动,或许会运行正常。(发生概率目测为15%左右)

3 结语

本C++数独求解器用到的知识有:
(1) 深度优先搜索DFS
(2) 随机数
(3) 没了。
如有错误之处,敬请广大读者指正,笔者自当感激不尽。

你可能感兴趣的:(C++数独求解器与生成器)