零、今天是10月5号,我写出了数独游戏的解啦!
一、题目描述(伪)
输入一个未解的数独盘,输出它的结果。
Input
共9行,每行9个数字(数字之间没有空格)
备注:如果这一格已经填有数字,相应的输入就是这个数字;如果这一格没有填数字,相应的输入为0。
Output
如果有解,输出共11行,参照示例。
如果无解,输出“您的输入似乎有误!检查一下吧!”。
示例:对于下面的例子有相应的输入
Input:
860300000
002084010
150000000
008200700
000809004
009050000
200000008
600008923
080020007Output:
二、事件起源
暑假以来,我一直在玩数独游戏。数独游戏太好玩了,但是一天一道新题,有时候我也会想偷懒,或者懒得算啥的。这才产生了自己写一个程序帮我解数独的想法。
好了,事情就是这么个事,那么我们到底应该怎么写嘞?
三、思路分析
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超时!
第三是使用体验,输入上没有乱七八糟的符号,只用数字输入即可;而输出形式美观,自动分成了九块。输入输出都十分友好!
用电脑帮我算以后我的解题速度也显著提升啦!毕竟电脑算的快嘛!
如果有需要的同学可以直接把代码搬走,没有关系的。但是,能不能留下一个小小的点赞甚至是关注呢(可怜状)自己的构思和自己的设计是真的真的很希望得到读者的认可!!!!
好了,以上就是今天分享的内容,同学们也可以把程序设计用在自己的日常生活中呀~!