数独游戏
题目描述
输入格式
The input test file will contain multiple cases. Each test case consists of a single line containing 81 characters, which represent the 81 squares of the Sudoku grid, given one row at a time. Each character is either a digit (from 1 to 9) or a period (used to indicate an unfilled square). You may assume that each puzzle in the input will have exactly one solution. The end-of-file is denoted by a single line containing the word “end”.
注意: 本题输入数据量较大,cin, getline可能会超时,建议使用scanf。
输出格式
For each test case, print a line representing the completed Sudoku puzzle.
输入样例
.2738..1..1...6735.......293.5692.8...........6.1745.364.......9518...7..8..6534.
......52..8.4......3...9...5.1...6..2..7........3.....6...1..........7.4.......3.
end
输出样例
527389416819426735436751829375692184194538267268174593643217958951843672782965341
416837529982465371735129468571298643293746185864351297647913852359682714128574936
题目思路
题目整体思路很容易想到:对于每一个空位置,尝试每一个可填数,填入后与同行、同列和同块的所有数字进行比对,看是否有重复;若有,更改当前填入数据,直到该位置无可能性,返回上一个填写的位置,更改数据。
难点1 怎样实现与同行同列同块数字的比较?
首先,我们可以通过一维数组存储输出输入的字符数组。这会为后面的遍历和查找带来方便。对该数组进行扩充,变成二维数组left,第一维存放该位置的数字,第二维存放该位置可放数字的可能性(如果数字确定可能性就为1)。
该位置可放数字的可能性通过函数assign对数组的更新来确定
bool assign(int left[][10],int idx,int lefted) { //假设idx位置的值是lefted left[idx][0]=1; for(int i=1;i<10;i++) { left[idx][i]=0; } left[idx][lefted]=1; int row=idx/9,col=idx%9; //在相应行判断有无重复元素 for(int i=0;i<9;i++) { int now=row*9+i; //若当前判断位置不是自身且该位置lefted是唯一可能性,说明该假设错误 if(now!=idx&&!remove(left,now,lefted)) { return false; } } //在相应列判断有无重复元素 for(int i=0;i<9;i++) { int now=i*9+col; if(now!=idx&&!remove(left,now,lefted)) { return false; } } //在相应块判断有无重复元素 for(int i=0;i<3;i++) { for(int j=0;j<3;j++) { int now=((row/3)*3+i)*9+(col/3)*3+j; if(now!=idx&&!remove(left,now,lefted)) { return false; } } } return true; }
难点2 怎样修改某位置的可能数字?
通过remove函数实现,注意,修改时要对可能性为1和2的位置进行特判
//从value数组中移除 value[idx][lefted]的可能性 bool remove(int left[][10],int idx,int lefted) { if(left[idx][lefted]==1)//若shudu[idx]有取lefted的可能 { left[idx][lefted]=0; left[idx][0]--; //该位置没有其他可能性了 if(left[idx][0]==0) { return false; } //该位置只有一种可能性了 else if(left[idx][0]==1) { for(int i=1;i<10;i++) { if(left[idx][i]!=0) { if(!assign(left,idx,i)) return false; else return true; } } } else return true; } else return true; }
难点3 怎样修改递归函数使得时间复杂度减低?
在选择试探的空位时,我们可以采用每次优先选择可能数字最少的进行试探,这样可以在极大程度上减少我们的搜索量。(快速失败法)
//选择shudu[]的空缺位置中可能性最少的位置进行匹配 bool search_match(int left[][10]) { int minx=0,minl=9; int flag=1; for(int i=0;i<81;i++) { if(left[i][0]!=1) { flag=0; if(left[i][0]<minl) { minl=left[i][0]; minx=i; } } } //当所有位置的可能性都为1时说明成功,结束递归 if(flag==1) { complete_shudu(left); return true; } for(int i=1;i<10;i++) { //遍历该位置的所有可能性 if(left[minx][i]!=0) { //设临时数组,将left[][]拷贝过来 int left_temp[81][10]; for(int j=0;j<81;j++) { for(int k=0;k<10;k++) { left_temp[j][k]=left[j][k]; } } //若i可以成功插入,且i插入后的数独可以完整填出来 if(assign(left_temp,minx,i)) { if(search_match(left_temp)) return true; } //若i不可以插入,则在left数组中删掉该可能性 left[minx][0]--; left[minx][i]=0; } } return false; }
完整代码
#include<stdio.h> #include<string.h> char shudu[100];//输入的数独数组 int left[82][10]={0};//对于数独第i号格子shudu[i][0]表示其可选择的数,shudu[i][j]表示是否可选数字j bool assign(int value[][10],int idx,int lefted); //对left数组进行初始化 void init(int left[82][10],char s[100]) { for(int i=0;i<81;i++) { if(s[i]=='.') { left[i][0]=9; for(int j=1;j<10;j++) { left[i][j]=1; } } else { left[i][0]=1; for(int j=1;j<10;j++) { left[i][j]=0; } left[i][(int)(s[i]-'0')]=1; } } } //打印数独数组 void print(char s[100]) { for(int i=0;i<81;i++) { printf("%c",s[i]); } printf("\n"); } //将整理好的数字填充到shudu数组中 void complete_shudu(int left[][10]) { for(int i=0;i<81;i++) { for(int j=1;j<10;j++) { if(left[i][j]!=0) { shudu[i]=j+'0'; break; } } } } //从value数组中移除 value[idx][lefted]的可能性 bool remove(int left[][10],int idx,int lefted) { if(left[idx][lefted]==1)//若shudu[idx]有取lefted的可能 { left[idx][lefted]=0; left[idx][0]--; if(left[idx][0]==0) { return false; } else if(left[idx][0]==1) { for(int i=1;i<10;i++) { if(left[idx][i]!=0) { if(!assign(left,idx,i)) return false; else return true; } } } else return true; } else return true; } //更新shudu[idx]的值 ,并更新left表 bool assign(int left[][10],int idx,int lefted) { //假设idx位置的值是lefted left[idx][0]=1; for(int i=1;i<10;i++) { left[idx][i]=0; } left[idx][lefted]=1; int row=idx/9,col=idx%9; //在相应行判断有无重复元素 for(int i=0;i<9;i++) { int now=row*9+i; //若当前判断位置不是自身且该位置lefted是唯一可能性,说明该假设错误 if(now!=idx&&!remove(left,now,lefted)) { return false; } } //在相应列判断有无重复元素 for(int i=0;i<9;i++) { int now=i*9+col; if(now!=idx&&!remove(left,now,lefted)) { return false; } } //在相应块判断有无重复元素 for(int i=0;i<3;i++) { for(int j=0;j<3;j++) { int now=((row/3)*3+i)*9+(col/3)*3+j; if(now!=idx&&!remove(left,now,lefted)) { return false; } } } return true; } //选择shudu[]的空缺位置中可能性最少的位置进行匹配 bool search_match(int left[][10]) { int minx=0,minl=9; int flag=1; for(int i=0;i<81;i++) { if(left[i][0]!=1) { flag=0; if(left[i][0]<minl) { minl=left[i][0]; minx=i; } } } //当所有位置的可能性都为1时说明成功,结束递归 if(flag==1) { complete_shudu(left); return true; } for(int i=1;i<10;i++) { //遍历该位置的所有可能性 if(left[minx][i]!=0) { //设临时数组,将left[][]拷贝过来 int left_temp[81][10]; for(int j=0;j<81;j++) { for(int k=0;k<10;k++) { left_temp[j][k]=left[j][k]; } } //若i可以成功插入,且i插入后的数独可以完整填出来 if(assign(left_temp,minx,i)) { if(search_match(left_temp)) return true; } //若i不可以插入,则在left数组中删掉该可能性 left[minx][0]--; left[minx][i]=0; } } return false; } int main() { scanf("%s",shudu); while(strcmp(shudu,"end")!=0) { //每次读入一个数独,都要将left数组重置 memset(left,0,sizeof(left)); init(left,shudu); //根据已确定的数字更新未确定块的可选情况 for(int i=0;i<81;i++) { if(shudu[i]!='.') { assign(left,i,(int)(shudu[i]-'0')); } } search_match(left); print(shudu); memset(shudu,NULL,sizeof(shudu)); scanf("%s",shudu); } return 0; }
总结
数独的解法中,最经典的是舞蹈链的解法,可以很好的降低时间复杂度,但是目前自己还看不懂这种做法。
链接先放在这里,以后再慢慢看:https://www.cnblogs.com/grenet/p/3163550.html
另,特别感谢我的小伙伴耐心讲解,这里附上他的博客地址:https://www.cztcode.com/2020/poj3074-summary/