数独游戏 | c++ | BFS

零、今天是10月5号,我写出了数独游戏的解啦!

一、题目描述(伪)

输入一个未解的数独盘,输出它的结果。

Input

共9行,每行9个数字(数字之间没有空格)

备注:如果这一格已经填有数字,相应的输入就是这个数字;如果这一格没有填数字,相应的输入为0。

Output

如果有解,输出共11行,参照示例。

如果无解,输出“您的输入似乎有误!检查一下吧!”。

示例:对于下面的例子有相应的输入

Input:

860300000
002084010
150000000
008200700
000809004
009050000
200000008
600008923
080020007

Output:


二、事件起源

        暑假以来,我一直在玩数独游戏。数独游戏太好玩了,但是一天一道新题,有时候我也会想偷懒,或者懒得算啥的。这才产生了自己写一个程序帮我解数独的想法。

好了,事情就是这么个事,那么我们到底应该怎么写嘞?


三、思路分析

        10月4日下午的马原课上,博主没带电脑,只好用纸和笔在脑中构建基本的框架,那天的笔记还在:

就让我本人给你们总结一下上面写的思路:

1、读入九行数据。

2、对每一个格子,如果是已解状态,则放之不管;如果是待解状态,则生成它的预选数串

3、若预选数串中只有一个数字,说明这一格只有这种填法,直接填入即可,并且将该格子标记为已解状态。

4、循环第2步和第3步数次。

5、然后再考虑用BFS求解。

Q:什么是预选数串?

A:这个是我自己起的名称,是格子里可能填的数字所组成的数串。例如上面的每日挑战的那张截屏中,第一行第三列的格子里可能填的数字是4和7,那么它的预选数串就是47,明白了吧!

Q:为什么不直接BFS,而是先执行第2步和第3步数次?

A:主要是为了减少BFS的深度,加快速度。

不难看出,这个程序如果写出来,核心就在BFS。只要会BFS,程序真的不难写出。


四、代码实现

        从昨晚11点动工,写到12点算是一个小时;上午满课,中午回来十二点半开始写,写到13:45写出来。这样看来,总共只花费了不到三个小时。这么一丁点时间支出却换来我内心巨大愉悦,我觉得很值。

1.1 结构体——单个小格子

typedef struct {			//单个小格子 
	int id;					//id=1表示已填,id=2表示未填
	char Filled;		    //Filled表示填入的数字 
	string Preselection;	//Preselection表示预选数列  
} Single;

说明:我原本是写成下面的模式
typedef struct {		    	//单个小格子 
	int id;				    	//id=1表示已填,id=2表示未填
    union {
        char Filled;	    	//Filled表示填入的数字 
	    string Preselection;	//Preselection表示预选数列  
    } info;
} Single;
因为union这块我确实不太熟悉,最后放弃,改成了别的模式。当然,union肯定更美观且节省空间。

1.2 结构体——整片数独盘

typedef struct {
	Single lattice[81];		//第i行第j列的格子位置为:k=9*i+j
	int cnt=0;				//cnt表示未填个数,当cnt=0时,表示已经全部填完了
} Sudoku;

2.1 函数——获取预选数串

//函数Get_Preselected_Sequence的作用是得到[i,j]位置的预选数串 
void Get_Preselected_Sequence(Sudoku *S, int i, int j) {
	int number[9]={0,0,0,0,0,0,0,0,0};

	//检测第i行的数字 
	for (int m=0; m<9; m++) {
		if ( (*S).lattice[9*i+m].id==1 ) 
			number[ (*S).lattice[9*i+m].Filled-'1' ] = 1;
	}

	//检测第j列的数字
	for (int m=0; m<9; m++) {
		if ( (*S).lattice[9*m+j].id==1 ) 
			number[ (*S).lattice[9*m+j].Filled-'1' ] = 1;
	}

	//检测所在中格的数字 
	//先找一下这个小格子所在的中格子
	int i_medium_start, j_medium_start;
	i_medium_start = i%3==0 ? i : ( i%3==1 ? i-1 : i-2 );
	j_medium_start = j%3==0 ? j : ( j%3==1 ? j-1 : j-2 );
    //再检测
	for (int m=0; m<3; m++) {
		for (int n=0; n<3; n++) {
			if ( (*S).lattice[9*(i_medium_start+m)+(j_medium_start+n)].id==1 ) 
				number[ (*S).lattice[9*(i_medium_start+m)+(j_medium_start+n)].Filled-'1' ] = 1;
		}
	}
	
	//生成预选数串:如果这个数字没出现过,就将其加入预选数串
	(*S).lattice[9*i+j].Preselection.erase();
	for (int m=0; m<9; m++) {
		if ( number[m]==0 ) {
			(*S).lattice[9*i+j].Preselection += (m+'1');
		}
	}
}

2.2 函数——生成 已经生成所有格子的预选数串 的数独盘

void Prepare(Sudoku *S) {
	void Get_Preselected_Sequence(Sudoku *, int, int);
	
	//每次S更新后,都可能有数字填入。而一旦有数字填入,意味着数独盘其他地方的预选数串可能发生改变,就要再次更新。 
	Sudoku RECORD;
	while (RECORD.cnt!=(*S).cnt) {
		RECORD = (*S);
		for (int m=0; m<9; m++) {
			for (int n=0; n<9; n++) {
					if ( (*S).lattice[9*m+n].id==2 ) {
					Get_Preselected_Sequence(S, m, n);

                	//如果预选数串中可选数量为0,说明这种填法有误,并将cnt标记为-1
					if ( (*S).lattice[9*m+n].Preselection.length()==0 ) {
						(*S).cnt = -1; return;
					}

	           	    //如果预选数串中可选数量为1,说明只有这个数字能填,直接填进去即可
						if ( (*S).lattice[9*m+n].Preselection.length()==1 ) {
						(*S).lattice[9*m+n].id = 1;
						(*S).lattice[9*m+n].Filled = (*S).lattice[9*m+n].Preselection[0];
						(*S).cnt--;
					}
				}			
			}
		}
	}
}

2.3 函数——输出数独盘

//函数Print的作用的输出数独块S 
void Print(Sudoku S) {
	for (int m=0; m<9; m++) {
		for (int n=0; n<9; n++) {
			if (S.lattice[9*m+n].id==1) 
				printf("%c ",S.lattice[9*m+n].Filled);
			if (n%3==2 && n!=8) printf("| ");
		}
		cout << endl;
		if (m%3==2 && m!=8) printf("---------------------\n");
	}
}

2.4 函数——按行优先的方式找到数独盘中第一个未填格子

int FindFirst(Sudoku S) {
	if (S.cnt>0)
		for (int m=0; m<9; m++) {
			for (int n=0; n<9; n++) {
				if (S.lattice[9*m+n].id==2) {
					return (9*m+n);
				}
			}
		}
}

主程序中——

1、读入数独SDK,SDK是Sudoku类型。

for (int m=0; m<9; m++) {
	for (int n=0; n<9; n++) {
			
		scanf("%c",&SDK.lattice[9*m+n].Filled);
        //如果读入的是0,id=2
		if (SDK.lattice[9*m+n].Filled=='0') {
			SDK.lattice[9*m+n].id=2;
			SDK.lattice[9*m+n].Preselection += SDK.lattice[9*m+n].Filled;
			SDK.cnt+=1;
		}
        //如果读入的不是0,id=1
		else {
			SDK.lattice[9*m+n].id=1;
		}
	}
	scanf("%*c");     //读回车
}

2、BFS写法

queue<Sudoku> QS;	
while ( QS.size() ) {

    //取顶
	Sudoku SDK_dynamic = QS.front();

    //弹顶
	QS.pop();

    //找第一个未填的格
	int f; f=FindFirst(SDK_dynamic);
    int number_of_numbers = SDK_dynamic.lattice[f].Preselection.length();

    //依次在这个未填的格子中填入所有可能的数字情况,然后加入队列中
	for (int m=0; m<number_of_numbers; m++) {
		Sudoku SDK_temporary;
		SDK_temporary = SDK_dynamic;

        //深度+1,id=1,Filled填入,cnt-1,这都不用解释吧
		SDK_temporary.depth += 1;
		SDK_temporary.lattice[f].id = 1;
		SDK_temporary.lattice[f].Filled = SDK_temporary.lattice[f].Preselection[m];
		SDK_temporary.cnt--;

        //因为加入了新的数字,所以每次加入队列之前要先更新预选数串
		Prepare(&SDK_temporary); Prepare(&SDK_temporary); Prepare(&SDK_temporary);

        //更新后还要检查,如果是-1,表示无解,不能入列;如果是0,成功解出,直接输出;如果大于0,说明还没完全解完,入列。
		if (SDK_temporary.cnt!=-1)
			if (SDK_temporary.cnt==0) {
				Print(SDK_temporary);
				return 0;
			}
			else QS.push(SDK_temporary);
	}
}

五、完整代码

害,思路就这么个思路,情况就这么个情况,也并不难,洒洒水咯~

#include<bits/stdc++.h> 
using namespace std;
typedef struct {			//单个小格子 
	int id;					//id=1表示已填,id=2表示未填
	char Filled;		    //Filled表示填入的数字 
	string Preselection;	//Preselection表示预选数列  
} Single;

typedef struct {
	Single lattice[81];		//第i行第j列的格子位置为:k=9*i+j
	int cnt=0;				//cnt表示未填个数,当cnt=0时,表示已经全部填完了
} Sudoku;
Sudoku SDK;

//函数Get_Preselected_Sequence的作用是得到[i,j]位置的预选数串 
void Get_Preselected_Sequence(Sudoku *S, int i, int j) {
	int number[9]={0,0,0,0,0,0,0,0,0};

	//检测第i行的数字 
	for (int m=0; m<9; m++) {
		if ( (*S).lattice[9*i+m].id==1 ) 
			number[ (*S).lattice[9*i+m].Filled-'1' ] = 1;
	}

	//检测第j列的数字
	for (int m=0; m<9; m++) {
		if ( (*S).lattice[9*m+j].id==1 ) 
			number[ (*S).lattice[9*m+j].Filled-'1' ] = 1;
	}

	//检测所在中格的数字 
	//先找一下这个小格子所在的中格子
	int i_medium_start, j_medium_start;
	i_medium_start = i%3==0 ? i : ( i%3==1 ? i-1 : i-2 );
	j_medium_start = j%3==0 ? j : ( j%3==1 ? j-1 : j-2 );
    //再检测
	for (int m=0; m<3; m++) {
		for (int n=0; n<3; n++) {
			if ( (*S).lattice[9*(i_medium_start+m)+(j_medium_start+n)].id==1 ) 
				number[ (*S).lattice[9*(i_medium_start+m)+(j_medium_start+n)].Filled-'1' ] = 1;
		}
	}
	
	//生成预选数串:如果这个数字没出现过,就将其加入预选数串
	(*S).lattice[9*i+j].Preselection.erase();
	for (int m=0; m<9; m++) {
		if ( number[m]==0 ) {
			(*S).lattice[9*i+j].Preselection += (m+'1');
		}
	}
}

//函数Prepare的作用是生成数独盘
//当预选数串中可选数量为1时,直接将其填入格子。
//如果预选数串中可选数量为0,说明这种填法有误。令cnt=-1,表示有误。
void Prepare(Sudoku *S) {
	void Get_Preselected_Sequence(Sudoku *, int, int);
	
	//每次S更新后,都可能有数字填入。而一旦有数字填入,意味着数独盘其他地方的预选数串可能发生改变,就要再次更新。 
	Sudoku RECORD;
	while (RECORD.cnt!=(*S).cnt) {
		RECORD = (*S);
		for (int m=0; m<9; m++) {
			for (int n=0; n<9; n++) {
					if ( (*S).lattice[9*m+n].id==2 ) {
					Get_Preselected_Sequence(S, m, n);

                	//如果预选数串中可选数量为0,说明这种填法有误,并将cnt标记为-1
					if ( (*S).lattice[9*m+n].Preselection.length()==0 ) {
						(*S).cnt = -1; return;
					}

	           	    //如果预选数串中可选数量为1,说明只有这个数字能填,直接填进去即可
						if ( (*S).lattice[9*m+n].Preselection.length()==1 ) {
						(*S).lattice[9*m+n].id = 1;
						(*S).lattice[9*m+n].Filled = (*S).lattice[9*m+n].Preselection[0];
						(*S).cnt--;
					}
				}			
			}
		}
	}
}

//函数Print的作用的输出数独块S 
void Print(Sudoku S) {
	for (int m=0; m<9; m++) {
		for (int n=0; n<9; n++) {
			if (S.lattice[9*m+n].id==1) 
				printf("%c ",S.lattice[9*m+n].Filled);
			if (n%3==2 && n!=8) printf("| ");
		}
		cout << endl;
		if (m%3==2 && m!=8) printf("---------------------\n");
	}
}

int FindFirst(Sudoku S) {
	if (S.cnt>0)
		for (int m=0; m<9; m++) {
			for (int n=0; n<9; n++) {
				if (S.lattice[9*m+n].id==2) {
					return (9*m+n);
				}
			}
		}
}


int main(){
	
	freopen("input.txt","r",stdin);
	void Prepare(Sudoku *S);
	void Print(Sudoku);
	int FindFirst(Sudoku);
	
	//读入待解数独S。
	//说明:读入0,表示这个空格是待填的。
	for (int m=0; m<9; m++) {
		for (int n=0; n<9; n++) {
			
			scanf("%c",&SDK.lattice[9*m+n].Filled);
			if (SDK.lattice[9*m+n].Filled=='0') {
				SDK.lattice[9*m+n].id=2;
				SDK.lattice[9*m+n].Preselection += SDK.lattice[9*m+n].Filled;
				SDK.cnt+=1;
			}
			else {
				SDK.lattice[9*m+n].id=1;
			}
		}
		scanf("%*c");
	}
	
	Prepare(&SDK);
	
	//简单的数独已经解完了 
	if (SDK.cnt==0) { Print(SDK); return 0; }
	
	//接下来需要往更难的方向走,考虑使用BFS 
	queue<Sudoku> QS;
	
	{
		int f; f=FindFirst(SDK);
		int number_of_numbers = SDK.lattice[f].Preselection.length();
		for (int m=0; m<number_of_numbers; m++) {
			Sudoku SDK_temporary;
			SDK_temporary = SDK;
			SDK_temporary.lattice[f].id = 1;
			SDK_temporary.lattice[f].Filled = SDK_temporary.lattice[f].Preselection[m];
			SDK_temporary.cnt--;
			Prepare(&SDK_temporary);
			if (SDK_temporary.cnt!=-1)
				if (SDK_temporary.cnt==0) {
					Print(SDK_temporary);
					return 0;
				}
				else QS.push(SDK_temporary);
		}
	}
	
	while ( QS.size() ) {
		Sudoku SDK_dynamic = QS.front();
		QS.pop();
		int f; f=FindFirst(SDK_dynamic);
		int number_of_numbers = SDK_dynamic.lattice[f].Preselection.length();
		for (int m=0; m<number_of_numbers; m++) {
			Sudoku SDK_temporary;
			SDK_temporary = SDK_dynamic;
			SDK_temporary.lattice[f].id = 1;
			SDK_temporary.lattice[f].Filled = SDK_temporary.lattice[f].Preselection[m];
			SDK_temporary.cnt--;
			Prepare(&SDK_temporary);
			if (SDK_temporary.cnt!=-1)
				if (SDK_temporary.cnt==0) {
					Print(SDK_temporary);
					return 0;
				}
				else QS.push(SDK_temporary);
		}
	}
	if ( !QS.size() ) 
		cout << "您的输入似乎有误!检查一下吧!\n";
	
	return 0;
}

六、一些后话

首先是准确率,我测试了很多大师难度的题,正确率100%,基本可以放心食用!

第二是速度,每个程序的平均运行时间都在一秒以内,不用担心BFS超时!

第三是使用体验,输入上没有乱七八糟的符号,只用数字输入即可;而输出形式美观,自动分成了九块。输入输出都十分友好!

用电脑帮我算以后我的解题速度也显著提升啦!毕竟电脑算的快嘛!

 如果有需要的同学可以直接把代码搬走,没有关系的。但是,能不能留下一个小小的点赞甚至是关注呢(可怜状)自己的构思和自己的设计是真的真的很希望得到读者的认可!!!!

好了,以上就是今天分享的内容,同学们也可以把程序设计用在自己的日常生活中呀~!

一个简单的解数独的小程序 //***求数独的解,参数mod为0或1,time0为搜索开始时的时间,mod=0时仅检查Data1中数独是否有解,有解则抛出1,mod=1时求出所有解并输出*** { int i,j,im=-1,jm,min=10; int mark[10]; for(i=0;i<9;i++) { for(j=0;j<9;j++) { if(Data1[i][j]) //如果该位置有数据则跳过 { continue; } int c=Uncertainty(i,j,mark); //如果该位置为空则先求不确定度 if(c==0) //如果不确定度为0则表示该数独无解 { return; } if(c<min) //得到不确定度最小的位置(第im行 第jm列 不确定度为min) { im=i; jm=j; min=c; } } } if(im==-1) //所有位置都已经确定,数独已经解出,按要求输出解 { if(mod==1) //显示所有解 { if(IsSolved()==true) { if(Solutions++<MAXANSNUM) { cout<<"第 "<<Solutions<<" 个 解:"<<endl; Display(1); } if((time(NULL)-time0)>TIMEOUT) { throw(Solutions); } } return; } else //只给出一个解 { throw(1); //跳出所有递归调用,返回1 } } Uncertainty(im,jm,mark); //从不确定度最小的位置开始解 for(i=1;i<=9;i++) { if(mark[i]==0) { Data1[im][jm]=i; //对不确定度最小的位置尝试可能的赋值 Search(mod,time0); //递归调用 } } Data1[im][jm]=0; } void Csudoku::Set(int n) //***随机生成数独,参数n表示数独中待填元素个数*** { srand((unsigned)time(NULL)); int i,j,k; do { for(i=0;i<9;i++) //随机给每行的某一个位置赋值 { for(j=0;j<9;j++) { Data1[i][j]=0; } j=rand()%9; Data1[i][j]=i+1; } } while(!Solve(0)); //按照随机赋的值给出一个解 for(k=0;k<n;) //从中随机去掉n个数据 { i=rand()%81; j=i%9; i=i/9; if(Data1[i][j]>0) { Data1[i][j]=0; k++; } } for(i=0;i<9;i++) //将生成的数独存入Data0数组 { for(j=0;j<9;j++) { Data0[i][j]=Data1[i][j]; } } }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值