题目大意:给出一篇文章,长度最多1000000,若干个关键词,关键词有可能重复。关键词不超过10000,每个关键词不超过50个字符。请问该文章包含多少个关键词。
这是AC自动机的入门题。首先将关键词分别插入到一棵trie树中,对每个关键词的对应的结束节点设置一个标记;第二步设置trie树中节点的fail指针。每个节点u都得有fail指针,指向某一个节点v,表示当u无法继续往下匹配时,u应该跳转的节点。节点u与v的字符相同,且根到v的字符串为根到u的字符串的最长后缀。如果u没有找到对应的v,则u的fail指针指向root。root的fail指针指向自身;第三步则是查询。查询时从根节点开始。每次根据当前的字符在当前节点的儿子中找到对应节点,如果没有找到,则跳转到当前节点的fail指针处,一直要找到某个节点,在它儿子中存在和当前字符匹配的节点,然后继续往下。当然如果到了根节点也没有找到,则查找下一个字符了。这里要注意的是,即使当前节点可以继续往下匹配,在当前节点往下匹配之前,我们需要另外拿一个指针,从当前节点出发,通过fail指针往上遍历,筛选是否有结束节点。因为有些关键词可能是当前节点到根的字符串的后缀。每个节点被筛选后可以做一个标记,下次筛选到这里时可以提前退出了。这样可以保证每个节点最多被筛选一次。
具体见代码:
#include<iostream> #include<cstdio> #include<cstring> using namespace std; #define MAXC 26 struct node { int cnt,fail,nxt[MAXC]; }trie[550000]; int root,tot=1,myq[550000],res=0,T,n,head,tail; char artical[1000005],word[55]; void insert(int r,char *s) { int len=strlen(s),val; for(int i=0;i<len;i++) { val=s[i]-'a'; if(trie[r].nxt[val]==0) trie[r].nxt[val]=++tot; r=trie[r].nxt[val]; } trie[r].cnt++; } void build(int r) { trie[r].fail=r; myq[tail++]=r; while(head<tail) { r=myq[head++]; for(int i=0;i<MAXC;i++) { int ch,p; if(ch=trie[r].nxt[i]) { myq[tail++]=ch; for(p=trie[r].fail;p!=root&&trie[p].nxt[i]==0;p=trie[p].fail); int tmp=trie[p].nxt[i];//tmp可能为空,因为p可能为根节点。 if(tmp&&tmp!=ch)trie[ch].fail=tmp;//防止ch的fail指针指向自己 else trie[ch].fail=root; } } } } void query(int r,char *s) { int len=strlen(s),val; for(int i=0;i<len;i++) { val=s[i]-'a'; while(r!=root&&trie[r].nxt[val]==0) r=trie[r].fail; r=trie[r].nxt[val]; if(r==0)r=root; for(int temp=r;temp!=root;temp=trie[temp].fail)//查询时要另一个指针从当前节点通过fail往上找。 { if(trie[temp].cnt==-1)break; //凡是找过的节点cnt设置为-1.所以遇到,可以提前退出。 res+=trie[temp].cnt; trie[temp].cnt=-1; } } } int main() { scanf("%d",&T); while(T--) { root=1; head=tail=0; tot=1; res=0; memset(trie,0,sizeof trie); scanf("%d",&n); for(int i=0;i<n;i++) {scanf("%s",word); insert(root,word); } scanf("%s",artical); build(root); query(root,artical); printf("%d ",res); } return 0; }