【YbtOj】数独游戏
题目链接:http://noip.ybtoj.com.cn/contest/16/problem/2
事实上,数独真的有毒,浪费了狂生我两个小时的时间!事实上,数独游戏的确很益智,呃,我是指这道题。
Saction A 输入/数据处理
输入有时候确实很头疼(对于输入的特别理解:https://www.cnblogs.com/bailikuangsheng/p/15108976.html),特别是输入后的处理。一旦遇到字符串的读入,并且字符串还要转换成数字,这题的水平就较高了,当这个转化为数字后的东西还要从一维变成二维,这题的水平就相当高了!
仔细阅读我对输入的特别理解,写出循环输入的代码之后,我们来看看如何把一个一维的字符串变成一个二维数组。
完成这个处理有两种方法:第一种先将字符串变成数字,放入一维数组,然后将一维转二维,第二种直接直接将字符串中的每个字符转换成数字存入一个二维数组。某些人想着第一种方法,却写着写着成了第二种方法,最后查虫的时候在哪里绕来绕去,最后造成了WA。
第一种方法太水了,并且很恶心,不仅内存消耗大并且不优美(当数据变多时也许会MLE或TLE),我们直接讲第二种方法。
既然要直接存入二维数组,那么就需要在这个字符还是一维的时候就确定它在二维里面的坐标。通常,我们在二维数组上使用伪平面直角坐标系的时候,会开两个for循环,循环变量i,j(或者是x,y)来表示某个数据在二维数组中的位置,这里也一样。
我们可以定义一个数据c,这个数据可以告诉字符串某个在二维数组里坐标为(i,j)的字符在字符串里的位置。这里将二维拆分一下,既然是9*9的各自,那么其行自然是以9个为一循环,这样来算第二行第一个数在二维上的位置应该是1*9+1=10位,这没错,但是字符串(数组也是)的第一位是第0位(尽管我觉得这样说很别扭),所以这个式子得改一下,应该是c=(i-1)*9+j。
取得了c就可以直接在二维数组中存入这个在字符串里第c为的数据。我们规定这个用来存放数独的二维数组名为a(似乎不太文雅,凑合着用吧),输入的字符串为s,那么在for循环中有:a[i][j]=s[c-1]-'0'(别问我-‘0’是什么,如果你不知道,那你的字符与字符串这个知识点得好好补补了)。
嗯!一切顺利,但还有一个小漏洞——这个数独不是完整的,其空缺位置由‘.’代替着,它们最好不要转化成数字的形式(‘.’在ascll中的代码是46,如果我没搞错的话),其实只要判断这个字符是不是在数字的范畴内就行了,当然这不是重点,重点还是字符串的储存是从第0位开始的,所以判断时应该写s[c-1]而不是s[c]!
OK!到目前为止,数据的转化已经完成了,但是在dfs开始之前,我们最好还是为其做一些准备工作。
整体观摩dfs不难发现,dfs中比较关键的几个要素:
- 终止条件
- 递归条件
- 搜索对象的状态
终止条件现在还不需要考虑,但是递归的条件很明显:这个地方不能有数字!这个地方不能写该行、列、九宫格内已有的数字!呵呵,这下好办了!如果你做过这道题,事情就变得简单起来:
//现在有1-m,m个数字,从中选出n个数字(n<=m),请问有几种选法(其中{1,2}和{2,1}不算同一种),此题俗称pmn
如果没有做过请参考:(咳咳,即将推出)
同理,这个地方要么有数字,要么没数字,这个数字要么已经被选了,要么没有被选。简单开三个bool数组就可以搞定了!一个bool数组x[]表示这个数字在行中是否被选,y[]表示这个数字在列中是否被选,g[]表示这个数字在该九宫格中是否被选。三个bool数组合在一起构成了整个数独二维的网络,十分便捷。代码如下:
1 x[i][a[i][j]]=y[j][a[i][j]]=g[check(i,j)][a[i][j]]=1;
其中check(,)函数用来判断其在哪个九宫格内,代码过于简单,就放在最终代码中展示。
A部分完整代码:
1 int main() 2 { 3 cin>>s; 4 while(s!="end") 5 { 6 7 memset(a,0,sizeof(a)); 8 memset(x,0,sizeof(x)); 9 memset(y,0,sizeof(y)); 10 memset(g,0,sizeof(g)); 11 for(int i=1;i<=9;i++) 12 for(int j=1;j<=9;j++) 13 { 14 c=(i-1)*9+j; 15 if(s[c-1]>='0' && s[c-1]<='9') 16 { 17 a[i][j]=s[c-1]-'0'; 18 x[i][a[i][j]]=y[j][a[i][j]]=g[check(i,j)][a[i][j]]=1; 19 } 20 } 21 dfs(1,1); 22 cout<<endl; 23 cin>>s; 24 } 25 return 0; 26 }
Section B 搜索
首先看一看这个搜索的结束条件:搜索到第10行。用人话说就是搜索超过第9行。在这里x,y已经被用,那么现在用xx与yy来表示搜索深度。
当到达终止条件时,即if(xx>9)成立时,就应该循环输出,然后return(跳出函数)。注意输出的数据应该是一位的,最外层的for循环里不要打cout<<endl。
接下来就是重头戏——搜索了!做过pmn,就知道,写一个for循环一一枚举1~9每个数字(也许我应该写成“11枚举”,以防你把那两个字看成了破折号),定义循环变量为i,那么当深度为xx,yy时表示(应该说假设)光标停在代表(xx,yy)的格子上,i是准备填入的数据,然而这个数据需要判断。有了x、y、g数组之后我们只用写一个if,判断i是否在这三个数组中的元素为0,如果是,那么递归,如果不是,那么i++。
在整个dfs中有两个容易忽略的关键。
第一,换行特判。当yy的值已经大于9,此时应该是xx+1,yy不变,反之是xx不变yy+1,代码如下:
1 if(yy<9) 2 dfs(xx,yy+1); 3 else 4 dfs(xx+1,1);
第二,该数位已填数。这个其实可以单独做个判断放在for循环填数字的前面。加入不判断,浪费时间,加入不单独判断,则又要单独开一个数组,麻烦。
看,其实到现在为止似乎还是挺简单的(呵呵),完整代码如下:
1 #include<bits/stdc++.h> 2 using namespace std; 3 string s; 4 int c,a[15][15],h,x[15][15],y[15][15],g[15][15]; 5 int check(int x,int y) 6 { 7 if(x<=3) 8 { 9 if(y<=3) 10 return 1; 11 if(y>3 && y<=6) 12 return 2; 13 return 3; 14 } 15 if(x>3 && x<=6) 16 { 17 if(y<=3) 18 return 4; 19 if(y>3 && y<=6) 20 return 5; 21 return 6; 22 } 23 if(y<=3) 24 return 7; 25 if(y>3 && y<=6) 26 return 8; 27 return 9; 28 } 29 void dfs(int xx,int yy) 30 { 31 if(xx>9) 32 { 33 for(int i=1;i<=9;i++) 34 for(int j=1;j<=9;j++) 35 cout<<a[i][j]; 36 return; 37 } 38 if(a[xx][yy]) 39 { 40 if(yy<9) 41 dfs(xx,yy+1); 42 else 43 dfs(xx+1,1); 44 return; 45 } 46 for(int i=1;i<=9;i++) 47 { 48 if(!x[xx][i] && !y[yy][i] && !g[check(xx,yy)][i]) 49 { 50 x[xx][i]=y[yy][i]=g[check(xx,yy)][i]=1; 51 a[xx][yy]=i; 52 if(yy<9) 53 dfs(xx,yy+1); 54 else 55 dfs(xx+1,1); 56 a[xx][yy]=0; 57 x[xx][i]=y[yy][i]=g[check(xx,yy)][i]=0; 58 //回溯,不讲你也应该懂 59 } 60 } 61 } 62 int main() 63 { 64 cin>>s; 65 while(s!="end") 66 { 67 //一定要记得清零 68 memset(a,0,sizeof(a)); 69 memset(x,0,sizeof(x)); 70 memset(y,0,sizeof(y)); 71 memset(g,0,sizeof(g)); 72 for(int i=1;i<=9;i++) 73 for(int j=1;j<=9;j++) 74 { 75 c=(i-1)*9+j; 76 if(s[c-1]>='0' && s[c-1]<='9') 77 { 78 a[i][j]=s[c-1]-'0'; 79 x[i][a[i][j]]=y[j][a[i][j]]=g[check(i,j)][a[i][j]]=1; 80 } 81 } 82 dfs(1,1); 83 cout<<endl; 84 cin>>s; 85 } 86 return 0; 87 }
Saction C 改进
这个程序交上去,发现竟然TLE了!
事实上,我们已经将判断即可行性的枝剪得很完美了,这个题目中也不存在最优性剪枝,那么只能从程序性能上去优化。虽然我们在if(xx>9)里面写了return,但是其很有可能会回溯,一旦它开始回溯,那就麻烦了!因为开始回溯之后的出口不再将是终止条件,而是if(a[xx][yy])里面的return!天!这回溯再递归再回溯……不TLE才怪呢!
因此,我么可以树一个旗子,当到达终止条件时,让旗子倒下,在dfs函数中加一个判断,如果发现旗子倒了,立马退出。当然这个旗子得是个全局变量,在输入与处理中将其定为0(1也可以,只是判断条件不同而已)。
在程序上改动如下:
if(h) return; if(h) if(xx>9) { for(int i=1;i<=9;i++) for(int j=1;j<=9;j++) cout<<a[i][j]; h=1; return; }
是吧,其实数独游戏还是有毒的,特别是在你发现你的输出总是全都是1的时候!