【YbtOj】虫食算
题目链接:http://noip.ybtoj.com.cn/contest/16/problem/3
说实话,我觉得这道题无论是题目(标题)还是题目(内容)都很恶心!没有一点扎实的数学基础,没有一点丰富的数学知识,没有一点巧妙的数学技巧根本做不出来!!!
Saction A 输入/数据处理
三行数据,可以用一个字符串数组来存储(只要你弄清楚字符串数组的第一个数据是行,第二个数据表示列),把这三行数据分别存进字符串的每一个组里(每一行),接着输入就结束了。
当然,输入部分最关键的不是输入,而是输入处理!
想要进行dfs,必不可少的就是对目标状态的控制,这里我们要提前考虑到输入的这个数据在搜索中的几个状态:第一,这个数在哪;第二,这个数是否已经被填过。
这个一维的字符串数组可以比拟成一个二维字符数组,但其稳定性是绝对远超字符数组,从右到左,从上到下,我们把这个字符串数组或者说是虫食算式整个遍历一遍,用v(visited)来记数字是否已被使用。在这个遍历的过程中,我们可以将这两个数组初始化,注意这里的数组里的每一项包含的是一类字母的值和状态,而不是虫食算式里的每个字母,那样太麻烦了。
在v数组中我们定义:0表示没有被用(可以用),1表示已被用(不能用填)。现在,我们将这个v数组借用一下,用它来存“这个字母是否已被遍历”,在初始化时置设定为1。由于需要一一对应(“11对应”也许更直观)的初始化,所以我们必须找到s[][]所代表的字母在字母表中的位置。因为是从右到左,从上到下,所以在循环变量为 i 和 j 的循环中s[j][i]可以用来表示指向的字符。根据ascll码,s[j][i]-'A'+1表示s[j][i]中的字母在字母表中排第s[j][i]-'A'+1位,因此只要让v[s[j][i]-'A'+1]里的值变为1即可。
考虑到在搜索的过程,其搜索的深度其实就是已经搜索了的字母的个数,而搜索的对象应该是一个字母,而不是整个虫食算式(废话,我们搜索的目的就是为了枚举每一个字母的值,当然,如果你想开N个for循环的话就是另外一个故事了,不用说这个故事的结局肯定很惨,惨到你连程序都写不出来,除非你特别牛),我们本来可以打一个26字母的跳转表,但因为这个式子的进制是不定的,很容易搜索越界,判断起来更麻烦,所以,我们可以开一个q(queue)数组,按照字母出现的顺序进行储存。
对于这个q数组,它的下标变量(如果你看不懂我在写什么,我建议你仔细阅读以下CCF对一维数组的讲解)可以单独用一个num存储,num从0开始计数,每当计入一个“字母”,就++num,为什么要先加在计入呢?很简单,我们最后的输出是按照字母表的顺序,而不是按照输入的顺序,所以我们想要计入的是这个字母在字母表中的排位,在搜索中作为一个类似指针的工具,详见下。
最后一个重要的问题!为什么初始v数组要初始为1?不是应该初始为0吗?没错我们的确应该将v数组全部初始为0,但是这样就出现了一个问题,我们希望q数组里面纪录一遍进入的字母就行了,那么重复的字母怎么记?重复的字母当然要被屏蔽掉,最好的方法就是在计入字母顺序的时候同事记录其是否已经入队,这个时候就可以借用v数组,在循环中加一个if判断该字母是否已经入队,聪明如你,你肯定知道怎么写,A部分代码如下:
1 int main() 2 { 3 cin>>n; 4 cin>>s[1]>>s[2]>>s[3]; 5 for(int i=n-1;i>=0;i--) 6 for(int j=1;j<=3;j++) 7 if(!v[s[j][i]-'A'+1]) 8 { 9 v[s[j][i]-'A'+1]=1; 10 q[++num]=s[j][i]-'A'+1; 11 } 12 memset(v,0,sizeof(v)); 13 memset(ans,-1,sizeof(ans)); 14 dfs(1); 15 return 0; 16 }
Saction B 搜索
搜索的框架很简单。
首先说一下终止条件。当搜索的深度大于进制数N(说白了就是把所有的字母搜了个遍),就return,但吸取了数毒“数独游戏”的经验,我们里一个flag,当终止条件成立时旗帜倒下,在dfs中第一个判断旗帜,如果倒下,立刻退出。
在终止条件到达时,我们可以输出答案,开一个for循环即可。
说完终止条件,我们再来看递归和回溯。我们定义搜索深度为x,在初始化里面,我们将v数组全部定义成了0,表示可以填。这里的v数组很巧妙。在某种意义上来说,它是一个空数组, 因为其中输入字母的顺序不定,所以v数组每一项的意义都是不定的,然而当它和q数组结合在一起时,就变成了一个重要的判断条件。if(!v[i])用来判断这个字母是否已经被填,接着就是递归和回溯的核心。
现在就要来谈谈q数组的奇妙转化!我们定义存储每个字母代表的数字的数组为ans,按照搜索的顺序来说应该是核q数组里面的相吻合,但是,答案的输出是要按字母表顺序的(字典序,嗯,这样更专业),但是如果把这个q数组中的值作为ans的下表变量的话,就不同了!当搜索深度为x时,q[x]表示正在搜索(枚举)在字母表里第q[x]的字母所代表的的数字,是一个顺序,将这个q[x]放在ans里面变成ans[q[x]],就变成了第q[x]个字母所代表的数字,是一个真实的数字!这个妙用不是用言语能够说清楚的!
到此,一旦找到没有被用过的数据,就可以继续递归,但在递归之前要做一个check,判断此数是否合法,这个在Saction C讲,B部分代码如下:
1 void dfs(int x) 2 { 3 if(h) 4 return; 5 if(x>n) 6 { 7 for(int i=1;i<=n;i++) 8 cout<<ans[i]<<" "; 9 h=1; 10 return; 11 } 12 for(int i=0;i<n;i++) 13 { 14 if(!v[i]) 15 { 16 v[i]=1,ans[q[x]]=i; 17 if(check()) 18 dfs(x+1); 19 v[i]=0,ans[q[x]]=-1; 20 } 21 } 22 }
Saction C check函数
作为本题核心中的核心,自然是一点都不能忽略!
所谓数据合法,无非就是看这个数据代入虫食算式是否成立。那么考虑数学功底的时刻到了!
不管三七二十一,我们先把目前的算式导出来:开一个for循环,从右往左,一次取出从上到下的三个数,手动运算!我们定义x,y,z分别表示加数,被加数,和(是sum不是and)。接下来,我们有需要调用到s字符串数组了,聪明如你,我就直接上代码了:
1 int x=ans[s[1][i-1]-'A'+1],y=ans[s[2][i-1]-'A'+1],z=ans[s[3][i-1]-'A'+1];
取出这三个值,首先应该判断,这三个字母是不是都已经变成了数字。在初始化中,我们将ans里的值全部定义成了-1,此刻只用一次判断就可以了。接着我们就要考虑,如何判断合法。离开整个虫食算式,我们单独看一列,不难想到高精加。在高精加中最主要的就是进位。对!进位就是这个判断的关键!
我们定义一个变量t,定义当t为-1时表示进位不确定,t为其他数值时,表示进位确定,且进位为t。
那么当t不等于-1时,x+y+t肯定要等于z才成立考虑到这是一个N进制数,所以要写(x+y+t)%n==z才行。同时,如果这一列在最左边,但是竟然产生了进位,那肯定错了,这个判断比较巧妙:i是递减的(看一看s是怎么存的就知道为什么是递减的了),所以当i==1时,才遍历到了最左边。如果有进位,那么x,y,t的和处以进制一定是1。是1啊!1表示true,这么说直接用if(i==1 && (x+y+t)/n)判断就可以了!当然,事实的确如此!
第二种情况,t等于-1,这个状态下的进位是不定的,此时这个进位可能是0,也可能是1,那么用一个if同时判断x,y的和加上0与1是否合法就行了,如果两个都非法,那就肯定没戏了。当然如果i==1,也需要判断是否能产生进位,不过这个只用看(x+y)/n就行了。
完整代码如下:
1 #include<bits/stdc++.h> 2 using namespace std; 3 string s[5]; 4 int n,ans[30],v[30],q[30],num,h; 5 bool check() 6 { 7 int t=0; 8 for(int i=n;i;i--) 9 { 10 int x=ans[s[1][i-1]-'A'+1],y=ans[s[2][i-1]-'A'+1],z=ans[s[3][i-1]-'A'+1]; 11 if(x!=-1 && y!=-1 && z!=-1) 12 { 13 if(t!=-1) 14 { 15 if((x+y+t)%n!=z) 16 return 0; 17 if(i==1 && (x+y+t)/n) 18 return 0; 19 t=(x+y+t)/n; 20 } 21 else 22 { 23 if((x+y)%n!=z && (x+y+1)%n!=z) 24 return 0; 25 if(i==1 && (x+y)/n) 26 return 0; 27 } 28 } 29 else 30 t=-1; 31 } 32 return 1; 33 } 34 void dfs(int x) 35 { 36 if(h) 37 return; 38 if(x>n) 39 { 40 for(int i=1;i<=n;i++) 41 cout<<ans[i]<<" "; 42 h=1; 43 return; 44 } 45 for(int i=0;i<n;i++) 46 { 47 if(!v[i]) 48 { 49 v[i]=1,ans[q[x]]=i; 50 if(check()) 51 dfs(x+1); 52 v[i]=0,ans[q[x]]=-1; 53 } 54 } 55 } 56 int main() 57 { 58 cin>>n; 59 cin>>s[1]>>s[2]>>s[3]; 60 for(int i=n-1;i>=0;i--) 61 for(int j=1;j<=3;j++) 62 if(!v[s[j][i]-'A'+1]) 63 { 64 v[s[j][i]-'A'+1]=1; 65 q[++num]=s[j][i]-'A'+1; 66 } 67 memset(v,0,sizeof(v)); 68 memset(ans,-1,sizeof(ans)); 69 dfs(1); 70 return 0; 71 }
嗯,逻辑很复杂,但它就是对的,事情往往就是这样奇怪!