HDU:2222-Keywords Search(AC自动机模板,匹配模拟)
Keywords Search
Time Limit: 2000/1000 MS (Java/Others)
Memory Limit: 131072/131072 K (Java/Others)
Problem Description
In the modern time, Search engine came into the life of everybody like Google, Baidu, etc.
Wiskey also wants to bring this feature to his image retrieval system.
Every image have a long description, when users type some keywords to find the image, the system will match the keywords with description of image and show the image which the most keywords be matched.
To simplify the problem, giving you a description of image, and some keywords, you should tell me how many keywords will be match.
Input
First line will contain one integer means how many cases will follow by.
Each case will contain two integers N means the number of keywords and N keywords follow. (N <= 10000)
Each keyword will only contains characters ‘a’-‘z’, and the length will be not longer than 50.
The last line is the description, and the length will be not longer than 1000000.
Output
Print how many keywords are contained in the description.
Sample Input
1
5
she
he
say
shr
her
yasherhs
Sample Output
3
解题心得:
- 首先就这个题来说只是AC自动机的一个模板题,今天刚把AC自动机学了,其实AC自动机并不是想象中的那么难,其实把两个前置技能字典树和KMP算法搞懂之后AC自动机也差不多水到渠成了。
- AC自动机也是字符串匹配,和KMP相比AC自动机处理的是多次匹配,如果使用KMP处理肯定会超时,而字典树是用来处理字符串的前缀的问题的,如果字符串过长,为了处理字符串中间的匹配,枚举字符串的每一个后缀来建树然后跑字典树肯定也会超时,这样结合字典树和KMP的AC自动机就出来了,既可以多次匹配还可以处理在字符串中间的匹配问题。
- 裸的AC自动机包含三个部分:
- 第一个部分就是字典树建树,AC自动机还是在字典树上跑的,只不过在建树的时候要注意一下,一个串结束时最后一个字符在树上的位置需要记录一下,这个不懂的先去看字典树。
- 第二个部分就是fail跳转,这个fail跳转其实就是参考的KMP的跳转思想,fail还是很形象的,失败之后就跳转,只不过KMP只是在一个线性表上面跳转,而fail跳转是在字典树上面跳转,在学fail跳转的时候要知道子节点的fail跳转位置都是通过父节点找到的,父节点寻找失败的时候就跳转父节点的fail,一直找不到跑到到根节点上,所以在写fail跳转的时候一定要弄清楚当前节点和父节点。
- 第三个就是询问,在寻找失败的时候要fail跳转,一直到找到第一个符合当前字符匹配的位置,找到的答案要标记,不然可能被重复找,但是上面说的是找到第一个符合匹配的位置,所以在找到之后还要继续跟着这个符合匹配的位置跳转,直到将树上所有的符合匹配的节点找完。
- 个人感觉这个fail标记像是在字符串去和字典树匹配,先从根节点开始往下面走,当在树上走不动的时候就往回走尝试从树的其他节点走,而fail其实就是标记的一个个你走不动的时候可以换路时,然后走另一条路,走不动就换,直到在根节点不能走了,那么就再从字符串的下一个字节再匹配,匹配成功的之后再将其他可以匹配的点也跳转过去。
#include<stdio.h>
#include<algorithm>
#include<queue>
#include<string.h>
using namespace std;
const int maxn = 1e6+100;
char s[maxn];
struct node
{
node *child[26];
node *fail;
int cnt;
node()//初始化每个节点
{
for(int i=0;i<26;i++)
child[i] = NULL;
fail = NULL;
cnt = 0;
}
};
node *root,*newnode,*current;
void insert_trie()//先建一颗字典树
{
int len = strlen(s),k;
current = root;
for(int i=0;i<len;i++)
{
int k = s[i] - 'a';
if(current->child[k] == NULL)
{
newnode = new node;
current->child[k] = newnode;
}
current = current->child[k];
}
current->cnt++;//记录以当前节点为字符串终点的数量
}
node *get_fail(node *p,int num)
{
if(p->child[num] != NULL)
return p->child[num];
else if(p == root)
return root;
else
return get_fail(p->fail,num);
}
void build_AC_automation()//建立fail跳转
{
current = root;
queue <node *> qu;//按照bfs搜索
qu.push(current);
while(!qu.empty())
{
current = qu.front();
qu.pop();
for(int i=0;i<26;i++)
{
if(current->child[i] != NULL)//如果当前的一个子节点不是空的那么就去找这个子节点匹配fail之后父节点跳转的位置
{
qu.push(current->child[i]);
if(current == root)//如果这个节点是根节点,那么子节点也只能往根节点上面跳了,没有其他办法了
current->child[i]->fail = root;
else
current->child[i]->fail = get_fail(current->fail,i);//子节点匹配fail失败之后跳转的位置是从父节点中找到的
}
}
}
}
void query()
{
int len = strlen(s),k,ans = 0;;
current = root;
for(int i=0;i<len;i++)
{
int k = s[i] - 'a';
//这一大坨都是为了找到第一可以匹配这个字符的位置
while(current->child[k] == NULL && current != root)//匹配失败是空的节点就往父节点fail跳转,找不到就会跳到根节点上面去
current = current->fail;
current = (current->child[k] == NULL)?root:current->child[k];//如果上面就已经跳到根节点就啥也不说了,不然就转移到这个可以转移的节点
node *temp = current;
//为何要写while循环,因为可能会有多个符合这个匹配的情况快要将树中符合情况的全给匹配完,这样才不会找漏
while(temp != root)//如果找到符合条件的节点就加上答案,然后标记这个已经找过的节点,然后就往其他符合的节点跳,找到的是已经被标记过的节点就直接跳出
{
if(temp->cnt >= 0)
{
ans += temp->cnt;
temp->cnt = -1;
}
else
break;
temp = temp->fail;
}
}
printf("%d\n",ans);
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
root = new node;
root->fail = root;
int n;
scanf("%d",&n);
for(int i=0;i<n;i++)
{
scanf("%s",s);
insert_trie();
}
build_AC_automation();
scanf("%s",s);
query();
}
}