HDU 2222 Keywords Search(AC自动机)

来源:互联网 发布:洞箫f调制作数据图 编辑:程序博客网 时间:2024/06/06 01:02

Keywords Search

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
15shehesayshrheryasherhs
 

Sample Output
3
 
    PS: 很久之前就想开AC自动机,一直没狠下心来去搞,今天晚上虽说昏昏沉沉的度过,不过还是慢慢的啃懂了AC自动机的最简单用法。用一道AC自动机比较经典的例题来当模板吧,还有如果你想很快的学会的话推荐一个博客【AC自动机】去认真的看,相信你也很快就会懂哒!不过要提前学会字典树和KMP噢。
    回归正题:
    【例题题意】
            题意很简单,首先给定n个长度不超过50的要查找字符串,最后一行给出一个模式串。问在模式串中出现了多少个待查找字符串,有坑点是这n个字符串中可能会有重复的。
    【解决方法】
            盲目的跑n遍KMP不知道可行不可行,不过算了一下极限复杂度是绝对要炸的。
            那么这时候就用到AC自动机了,AC自动机用通俗的话来说就是多个数据结构放在了一起,这次是KMP+字典树而已,如果你已经学会了KMP,相信你理解AC自动机中使用BFS建立每个节点的fail指针和最后查找字符串的时候就不会那么难了。
            具体的做法,我们先把给定的n个待查字符串以字典树的形式存储下来,以下是建立字典树的过程。
//建立字典树char str[1000100];struct node{    int count;    node *next[26];     node *fail;    void init(){          //初始化函数,这样写会节省大量代码量        count = 0;        //因为n个待查字符串会有重复的,所以我们设立一个count来统计一下数量        fail = NULL;        for(int i=0;i<26;i++)            next[i] = NULL;    }}*root;void insert(char *str){  //传进来的str是每个待查找字符串    int len = strlen(str);    node *p = root;    for(int i=0;i<len;i++){        int pos = str[i] - 'a';        if(p->next[pos] == NULL){            p->next[pos] = new node;            p->next[pos]->init();            p = p->next[pos];        }        else            p = p->next[pos];    }    p->count++;}
            建好字典树之后,我们用bfs去更新字典树上每个节点的fail指针,fail指针中存储的是当p->next[pos]查找失败时应当跳转的指针p=p->fail.相当于KMP算法之中的next数组,只不过KMP算法的next数组以数组的形式存储了下来,这里我们用指针的形式存储下来,应当跳转的位置。
//构造失败指针(fail指针)//其实这个过程很简单,我们归结为一句话//设这个节点上的字母为C,沿着他父亲的失败指针走,直到走到一个特殊的节点,//恰好这个特殊的节点的儿子中也有字母为C的节点。然后把当前节点的失败指针指向那个字母也//为C的儿子。如果一直走到了root都没找到,那就把失败指针指向rootvoid Getfail(){    node *p = root,*son,*temp;    queue<node*>Q;                    //节点队列    Q.push(p);    while(!Q.empty()){        temp = Q.front();Q.pop();        for(int i=0;i<26;i++){            son = temp->next[i];            if(son != NULL){                if(temp == root)                       son->fail = root;                 else{                    p = temp->fail;                    while(p){                        if(p->next[i]){                            son->fail = p->next[i];                            break;                        }                        p = p->fail;                    }                    if(p == NULL)     //一直找到最后为NULL了还没找到,就直接指向root                        son->fail = root;                }                Q.push(son);          //每更新了一个节点,把这个节点存到队列里,方便去更新它的子孙节点            }        }    }}
            直到fail指针(即失败指针)建立好之后,最后只剩下去匹配了,他是怎么匹配的呢?我们看代码。
//查找模式串int cal_ans(){    //Getfail();    int len = strlen(str),num = 0;    node *p,*temp;    p = root;    for(int i=0;i<len;i++){             //遍历模式串        int pos = str[i] - 'a';        while(!p->next[pos] && p!=root) //将fail指针指到最后有特殊的节点的节点            p = p->fail;        p = p->next[pos];                       if(p == NULL)            p = root;        temp = p;        while(temp!=root){            if(temp->count>0){         //如果count大于0,更新答案,并删掉count值                num += temp->count;                temp->count = 0;            }            else                break;            temp = temp->fail;         //遍历他所有的失败节点,即以当前字母作为结尾的所有答案        }    }    return num;}
          如果你还没有看明白的话,请手动画图,把例题的样例的字典树画在纸上,然后模拟上述代码,半个小时之内你就会明白AC自动机的所有知识点。如果再不明白,去我上面给的博客,他有已经做好的例图,结合他说的过程,你就会明白啦!

AC Code
#include <algorithm>#include <iostream>#include <numeric>#include <cstring>#include <iomanip>#include <string>#include <vector>#include <cstdio>#include <queue>#include <stack>#include <cmath>#include <map>#include <set>#define LL long long#define lson l,m,rt<<1#define rson m+1,r,rt<<1|1const LL M = 100099;const double esp = 1e-6;const double PI = 3.14159265359;const LL INF = 0x3f3f3f3f;using namespace std;//建立字典树char str[1000100];struct node{    int count;    node *next[26];     node *fail;    void init(){          //初始化函数,这样写会节省大量代码量        count = 0;        //因为n个待查字符串会有重复的,所以我们设立一个count来统计一下数量        fail = NULL;        for(int i=0;i<26;i++)            next[i] = NULL;    }}*root;void insert(char *str){  //传进来的str是每个待查找字符串    int len = strlen(str);    node *p = root;    for(int i=0;i<len;i++){        int pos = str[i] - 'a';        if(p->next[pos] == NULL){            p->next[pos] = new node;            p->next[pos]->init();            p = p->next[pos];        }        else            p = p->next[pos];    }    p->count++;}//构造失败指针(fail指针)//其实这个过程很简单,我们归结为一句话//设这个节点上的字母为C,沿着他父亲的失败指针走,直到走到一个特殊的节点,//恰好这个特殊的节点的儿子中也有字母为C的节点。然后把当前节点的失败指针指向那个字母也//为C的儿子。如果一直走到了root都没找到,那就把失败指针指向rootvoid Getfail(){    node *p = root,*son,*temp;    queue<node*>Q;                    //节点队列    Q.push(p);    while(!Q.empty()){        temp = Q.front();Q.pop();        for(int i=0;i<26;i++){            son = temp->next[i];            if(son != NULL){                if(temp == root)                       son->fail = root;                 else{                    p = temp->fail;                    while(p){                        if(p->next[i]){                            son->fail = p->next[i];                            break;                        }                        p = p->fail;                    }                    if(p == NULL)     //一直找到最后为NULL了还没找到,就直接指向root                        son->fail = root;                }                Q.push(son);          //每更新了一个节点,把这个节点存到队列里,方便去更新它的子孙节点            }        }    }}//查找模式串int cal_ans(){    Getfail();    int len = strlen(str),num = 0;    node *p,*temp;    p = root;    for(int i=0;i<len;i++){             //遍历模式串        int pos = str[i] - 'a';        while(!p->next[pos] && p!=root) //将fail指针指到最后有特殊的节点的节点            p = p->fail;        p = p->next[pos];                       if(p == NULL)            p = root;        temp = p;        while(temp!=root){            if(temp->count>0){         //如果count大于0,更新答案,并删掉count值                num += temp->count;                temp->count = 0;            }            else                break;            temp = temp->fail;         //遍历他所有的失败节点,即以当前字母作为结尾的所有答案        }    }    return num;}int main(){    int t,n;    scanf("%d",&t);    while(t--){        root = new node;        root->init();        scanf("%d",&n);        for(int i=0;i<n;i++){            scanf("%s",str);            insert(str);        }        scanf("%s",str);        printf("%d\n",cal_ans());    }    return 0;}



0 0