HUST 1605 Gene recombination
隐式图搜索
训练的题目,题意:输入n表示串(串为基因,只会出现ACGT)的长度,下面两行长度为n的串,第一个为起始串,第二个为目标串。
对串能做两种操作。1.将头元素移动到尾部。2.最前面两个元素交换位置。从起始串到目标串的最少操作次数是多少,输出
这题一看,觉得是DP,后来发了两三分钟的样子想到了是搜索。对于当前的串,它是一个状态,通过两个操作,能产生两个新的状态,所以这个过程就可以建图,搜索,找出两点间的最短路。注意这里不是树,因为很容易想到,这个图是可以有环的。另外,可以大致计算到状态数是很多的(串长最大为12),所以不能显式建图,当然也没必要显式建图,因为很多点(状态)是不会去到的
很快打出代码,过sample,提交,超时。超时还是预料到的,因为没有做剪枝,会对相同状态搜索
然后就想怎么剪枝,显然是要记录那些状态被搜索过,这个是最初想到的(但为什么不写呢,反省)
注意这里的状态时一个字符串,并不好表示它,所以可以想到转化为数字,状态压缩!因为只有ACGT,所以对应为0123,那么整个串就是一个四进制数。
状态数最大可以对应到4^12的,但是悲剧的是,这题会卡内存,不能用vis数组来标记,另外,这个图,并不是全部状态都会去到。
那些怎么办呢?用vector?把搜索的状态存在vector中,每得到一个状态,就去vector里面扫描一次看看是否存在?这样依然是超时的。那么可用set?直接在set中查找这个状态?后来我没往这里想了,因为从最初我就想到了哈希(但是为什么不写呢?反省!),哈希应该是最快的。
最后就静下来写哈希,写哈希怎么写呢?状态是一个字符串,它可以变为一个四进制数,两者是完全等价的,那么是用这个四进制数去哈希好呢,还是用字符串去哈希好呢?最后选择了用字符串去哈希,使用BKDHash去做,冲突少效果好
为了再快点,在哈希的时候,是用字符串去计算映射地址的,但是地址里保存是的那个四进制数!
(另外:处理冲突,使用了静态链表,即数组模拟)
到了这里,就没什么困难了,就是搜索了,搜索时,起始状态入队,次数为0,以后每次搜索,次数加1即可
具体步骤:
1.得到一个新的字符串,在哈希表中查找它,如果哈希表中已经有了,就跳过,证明这个状态已经被搜索过,不要再搜它,就算搜它,也不是最优解,因为搜索时是按照步数从小到大来的。当前步数一定比之前的大
2.如果在哈希表中没有这个状态,那么插入哈希表中,并且这个状态要入队。
3.所以队列的元素应该记录两个信息,状态,搜到这个状态需要多少次。
3.可以知道一个状态不会两次入队,一个状态一旦入队,它的次数就是最小次数
另外,网上有人说,这个宽度搜索需要优先队列,这是多余的,不需要,使用优先队列的目的是为了每次出队的元素都是次数最少的,但是整个搜索的过程就是从次数递增的规则来的,所以后来入队的元素的次数一定 >= 之前的元素的次数,况且,一个状态不会二次入队
#include <cstdio> #include <cstring> #include <queue> using namespace std; #define N 3000000 //这东西,最少要4,5百万,3百万都不行,会越界,存不下所有状态 //数组并不是开小点或者开大点好,因为这个数组是对应哈希的,数组的大小 //会多少影响到哈希表的冲突,测试了一下,1千万到3千万间时间较好 #define LEN 15 #define INF 0x3f3f3f3f #define MM 0x7fffffff int n; char S[LEN] , T[LEN]; int st,ed; struct State{ //压缩成数字来表示一个状态 int s,c; }; int head[N+10] , tot; //哈希 struct edge{ int s,next; }e[N+10]; typedef struct edge edge; typedef struct State State; queue<State>q; void add(int index ,int s) { e[tot].s=s; e[tot].next=head[index]; head[index]=tot++; } int cton(char *str) //字符串转数字 { int c=1 , ans=0; for(int i=n-1; i>=0; i--) { int s; if(str[i]=='A') s=0; else if(str[i]=='C') s=1; else if(str[i]=='G') s=2; else s=3; ans += s*c; c *= 4; } return ans; } void ntoc(int x , char *str) //数字转字符串 { for(int i=n-1; i>=0; i--) { int s=x%4; if(s==0) str[i] = 'A'; else if(s==1) str[i] = 'C'; else if(s==2) str[i] = 'G'; else str[i] = 'T'; x /= 4; } str[n] = '\0'; } int BKDHash(char *str) //字符串的哈希映射用BKD { int len = strlen(str); int seed = 131; int res = 0; for(int i=0; i<len; i++) res = res*seed + str[i]; res = (res&MM)%N; return res; } int find(char *str , int s) //查找 { int index = BKDHash(str); for(int k=head[index]; k!=-1; k=e[k].next) if(e[k].s == s) return k; add(index,s); //在哈希表中查不到这个状态,是一个新的状态,添加进哈希表中 return -1; } void BFS() { int res; int index; int i,j; char s1[LEN] , s2[LEN]; State sta; while(!q.empty()) { State u=q.front() , v; ntoc(u.s,s1); q.pop(); //第一种转换方式,头元素放到尾部 for(i=0,j=1; j<n; i++,j++) s2[i] = s1[j]; s2[n-1] = s1[0]; s2[n] = '\0'; v.s=cton(s2); index = find(s2,v.s); if(index == -1) //一个新的状态 { v.c = u.c+1; q.push(v); if(v.s == ed) { res=v.c; break; } } //第二种转换方式,两个头元素交换 for(int i=2; i<n; i++) s2[i] = s1[i]; s2[0] = s1[1]; s2[1] = s1[0]; s2[n]='\0'; v.s=cton(s2); index = find(s2,v.s); if(index == -1) { v.c=u.c+1; q.push(v); if(v.s == ed) { res=v.c; break; } } } printf("%d\n",res); } int main() { //freopen("fuckcase.txt","r",stdin); //freopen("output.txt","w",stdout); while(scanf("%d",&n)!=EOF && n) { scanf("%s%s",S,T); if(!strcmp(S,T)) { printf("0\n"); continue; } tot=0; memset(head,-1,sizeof(head)); st=cton(S); ed=cton(T); int index = BKDHash(S); //字符串的哈希用BKD add(index,st); State sta; sta.s=st; sta.c=0; while(!q.empty()) q.pop(); //忘记清空队列,wa了很多次,后来发现单case输入和多case输入不同,才发现 //引以为戒 q.push(sta); BFS(); } return 0; }