P2536 [AHOI2005] 病毒检测
P2536 [AHOI2005] 病毒检测
题目描述
科学家们在 Samuel 星球上的探险仍在继续。非常幸运的,在 Samuel 星球的南极附近,探险机器人发现了一个巨大的冰湖!机器人在这个冰湖中搜集到了许多 DNA 片段运回了实验基地。
科学家们经过几个昼夜的研究,发现这些 DNA 片段中有许多是未知的病毒!
每个 DNA 片段都是由 A
、C
、T
、G
组成的序列。科学家们也总结出了 Samuel 星球上的“病毒模版片段”。一个模版片段是由 A
、C
、T
、G
的序列加上通配符 *
和 ?
来表示。其中 *
的意思是可以匹配上 000 个或任意多个字符,而 ?
的意思是匹配上任意一个字母。
如果一个 DNA 片段能够和“病毒模版片段”相匹配,那么这个 DNA 片段就是未知的病毒。
例如,假设 “病毒模版片段”为 A*G?C
。DNA 片段:AGTC
,AGTGTC
都是未知的病毒,而 DNA 片段 AGTGC
则不是病毒。
由于,机器人搜集的这些 DNA 片段中除去病毒的其他部分都具有非常高的研究价值。所以科学家们希望能够分辨出其中哪些 DNA 片段不是病毒,并将不是病毒的 DNA 片段运回宇宙空间站继续进行研究。
科学家将这项任务交给了小联。现在请你为小联编写程序统计哪些 DNA 片段不是病毒。
输入格式
共 N+2N+2N+2 行输入。
第一行有一个字符串,由 A
、C
、T
、G
、*
、?
组成,表示“病毒模版片段”。“病毒模版片段”的长度不超过 100010001000。
第二行有一个整数 NNN,表示机器人搜集到的 DNA 片段的数目。
随后的 NNN 行,每一行有一个字符串,由 A
、C
、T
、G
组成,表示一个 DNA 片段。
输出格式
只有一行输出,为整数 MMM,即不是病毒的 DNA 片段的数目。
输入输出样例 #1
输入 #1
A*G?C
3
AGTC
AGTGTC
AGTGC
输出 #1
1
说明/提示
输入中的 DNA 片段 AGTGC
不是病毒。
对于所有数据,0<N<5000 < N < 5000<N<500。
特别的:
- 每个 DNA 片段的长度不超过 500500500;
- “病毒模版片段”和 DNA 片段的长度都至少为 111。
解析
本题基本可以看成一个病毒标准串与多个DNADNADNA模式串的匹配。
当想到是多个字符串与单个字符串匹配时,我们可以想到 Trie树Trie树Trie树 。
(Trie树Trie树Trie树不在此详细介绍)
然而难度在于,标准串的形式不唯一,复杂多变不可定。那么我们思考,能否在由多个DNADNADNA模式串形成的Trie树Trie树Trie树上搜索符合标准串格式的串,并统计个数。
那么这里使用深搜(dfsdfsdfs),将标准串遍历一遍来实现搜索。
I考虑搜索的策略:
假设当前扫到了标准串 vvv 的某一位 vpv_pvp。
注:树上点的对应子节点是指:表示的字母 与 接下来搜索的标准串的字母相同 的子节点
①若 vpv_pvp 是字母,则直接搜索下一位 vp+1v_{p+1}vp+1,树上搜索的位置也来到对应的子节点(若没有对应子节点的话将不会进行搜索)。因为此时你根本没有任何其他搜索的选择。
②若 vpv_pvp是问号(???),此时我们拥有 444 个搜索的方向(A、T、C、GA、T、C、GA、T、C、G),对于此我们把树上搜索的位置分别移到 444 个子节点,再分别搜索接下来的 vp+1v_{p+1}vp+1,进行进一步深搜。
③若 vpv_pvp 是星号(∗*∗),其难度在于它可代替任意长度的字符串。但是想到,问号可以取代一位,那么“∗”“*”“∗”就是若干个“?”“?”“?”,对此我们每次搜索时,分别搜索 444 个子节点,而 ppp 的情况有两种:要么移到 vp+1v_{p+1}vp+1(相当于“∗”“*”“∗”取代字母到此为止了),要么保持 vpv_{p}vp(相当于下一位还是由“∗”“*”“∗”取代产生的)。不必担心“?”“?”“?”取代的搜索会无穷尽,因为Trie树Trie树Trie树是有限的,导致串无法无限的搜索。
当然别忘了“?”“?”“?”还能表示没有字母,所以直接搜索 vp+1v_{p+1}vp+1 且不考虑移动树上的搜索点即可。
而终止条件就是 p=len(v)p=len(v)p=len(v) 的时候,此时标准串已经被扫完,只需记录此时以当前树上搜索点结尾的模式串有几个即可。
当然,面对拗口的解释并不足以理解,但在代码中将会有详细注释更加强理解
II考虑优化
显然在深搜时会出现相同状态的重复搜索,进而导致超时。解决这种问题的最佳方法就是记忆化搜索记录已经搜过的状态,再次遇到时就不必继续重复的搜索。
III代码及注释
#include<bits/stdc++.h>
using namespace std;
int n;
char dis[4]={'A','C','T','G'};
string v;
string s;
int trie[250010][40];
bool vis[1000][250010];
int tot;
void append(){//建Trie树的基本过程不再赘述int p=0;int len=s.length();for(int i=0;i<len;i++){if(!trie[p][s[i]-'A']){tot++;trie[p][s[i]-'A']=tot;}p=trie[p][s[i]-'A'];}trie[p][29]++;//记录以该节点结尾的模式串个数
}
int ans;
int l; //v的长度
void dfs(int p,int t){//p为模式串匹配到的位置 [0,l),t为trie树的点位 if(p==l){//匹配完了ans+=trie[t][29];trie[t][29]=0;//清零以避免重复加。//因为一个模式串可能有多种方法匹配。return;}if(vis[p][t]) return;//搜过了这种状态不搜了vis[p][t]=true;//标记该状态已搜if('A'<=v[p]&&v[p]<='Z'&&trie[t][v[p]-'A']){//v[p]是字母且该节点有对应儿子dfs(p+1,trie[t][v[p]-'A']);}else if(v[p]=='?'){//v[p]是"?"时有四种选择for(int i=0;i<=3;i++){if(trie[t][dis[i]-'A']){dfs(p+1,trie[t][dis[i]-'A']);}}}else if(v[p]=='*'){dfs(p+1,t);//"*"表示没有字母for(int i=0;i<=3;i++){if(trie[t][dis[i]-'A']){dfs(p+1,trie[t][dis[i]-'A']);//"*"代替完了dfs(p,trie[t][dis[i]-'A']);//"*"继续代替若干个字母}}}
}
int main(){cin>>v>>n;for(int i=1;i<=n;i++){cin>>s;append();}l=v.length();dfs(0,0);cout<<n-ans;//ans是匹配得上的个数,而题目求匹配不上的个数return 0;
}
IV其他的方法
据同机房的大佬LEE表示,,存在一种将 vvv 所有情况建成有自环的 Trie树Trie树Trie树。但本蒟蒻还不明白……