Keywords Search HDU - 2222(AC自动机模板)

题意:

给定 n个长度不超过 50的由小写英文字母组成的单词准备查询,以及一篇文章,问:文中出现了多少个待查询的单词。多组数据。

题目:

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自动机模板匹配多模式串问题。单模式串匹配用KMP算法,那么多模式串匹配,不可能一个一个字符串建立失配数组,复杂度太高0(m*n),所以对多模式串建成一个trie树,再建立失配数组,复杂度为o(n n \sqrt{n} n )。AC自动机详细分析见代码后。

AC代码:

#include<stdio.h>
#include<string.h>
#include<algorithm>
using  namespace std;
const int N=5e5+10;
int ans,tot,dp[N],ch[N][30],bo[N],que[N];
void makes(char *s)/**将所有模式串构建成一颗tire树*/
{
    int u=1,len=strlen(s);
    for(int i=0; i<len; i++)
    {
        int c=s[i]-'a';
        if(!ch[u][c])
            ch[u][c]=++tot;
        u=ch[u][c];
    }
    bo[u]++;
    return ;
}
void bfs()/**通过BFS构建dp(失配)数组*/
{
    for(int i=0; i<26; ++i)
        ch[0][i]=1;/**为了方便将0的所有转移边都设为根节点1*/
    que[1]=1,dp[1]=0;/**若在根节点失配,则无法匹配字符*/
    for(int x=1,y=1; x<=y; x++)
    {
        int u=que[x];
        for(int i=0; i<=25; i++)
        {
            if(!ch[u][i])/**如果不存在u的转移边i时,失配,KMP*/
                ch[u][i]=ch[dp[u]][i];///优化
            else
            {
                que[++y]=ch[u][i];/**若有这个儿子则将其加入队列中*/
                dp[ch[u][i]]=ch[dp[u]][i];
            }
        }
    }
}
void find(char *s)
{
    int u=1,len=strlen(s),c,k;
    for(int i=0; i<=len; i++)
    {
        c=s[i]-'a';
        k=ch[u][c];
        while(k>1)
        {
            ans+=bo[k];
            bo[k]=0;
            k=dp[k];
        }
        u=ch[u][c];
    }
    return ;
}
int main()
{
    int t,n;
    char s[N<<1];
    scanf("%d",&t);
    while(t--)
    {
        ans=0,tot=1;
        memset(bo,0,sizeof(bo));
        memset(ch,0,sizeof(ch));
        scanf("%d",&n);
        while(n--)
        {
            scanf("%s",s);
            makes(s);
        }
        bfs();
        scanf("%s",s);
        find(s);
        printf("%d\n",ans);
    }
    return 0;
}

AC自动机详细分解:

ac自动机很神奇,在于这个算法中失配指针的妙处(好比kmp算法中的next数组),说它高深,是因为这个不是一般的算法,而是建立在两个普通算法的基础之上,而这两个算法就是kmp与字典树。ac自动机其实就是一种多模匹配算法,那么你可能会问什么叫做多模匹配算法。下面是我对多模匹配的理解,与多模与之对于的是单模,单模就是给你一个单词,然后给你一个字符串,问你这个单词是否在这个字符串中出现过(匹配),这个问题可以用kmp算法在比较高效的效率上完成这个任务。那么现在我们换个问题,给你很多个单词,然后给你一段字符串,问你有多少个单词在这个字符串中出现过,当然我们暴力做,用每一个单词对字符串做kmp,这样虽然理论上可行,但是时间复杂度非常之高。

建立tire树(就是tire树模板)这个简单

void makes(char *s)/**将所有模式串构建成一颗tire树*/
{
    int u=1,len=strlen(s);
    for(int i=0; i<len; i++)
    {
        int c=s[i]-'a';
        if(!ch[u][c])
            ch[u][c]=++tot;
        u=ch[u][c];
    }
    bo[u]++;
    return ;
}
建立后此时多模式串为一个字典树,将存在公共前缀的字符串“合并”,可以用一个数组(每个新点插入++tot)表示树上所有点。

建一个树的图:

在这里插入图片描述

建立该字典树的失配数组(bfs(队列)+KMP+前缀数组)重点

void bfs()/**通过BFS构建dp(失配)数组*/
{
    for(int i=0; i<26; ++i)
        ch[0][i]=1;/**为了方便将0的所有转移边都设为根节点1*/
    que[1]=1,dp[1]=0;/**若在根节点失配,则无法匹配字符*/
    for(int x=1,y=1; x<=y; x++)
    {
        int u=que[x];
        for(int i=0; i<=25; i++)
        {
            if(!ch[u][i])/**如果不存在u的转移边i时,失配,KMP*/
                ch[u][i]=ch[dp[u]][i];///优化
            else
            {
                que[++y]=ch[u][i];/**若有这个儿子则将其加入队列中*/
                dp[ch[u][i]]=ch[dp[u]][i];
            }
        }
    }
}
  • 首先建立的是一个tire树,一个结点上可能连多个节点,所以这里用BFS(用进入队列的方式,处理每一个点),最后将tire树建立成一个可与给定串匹配的失配数组
  • 构建失配数组(在代码中我写的是dp数组),使当前字符失配时跳转到另一段(最近匹配的位置)【root开始每一个字符(前缀)都与当前已匹配字符段某一个后缀完全相同且长度最大的位置继续匹配】如同KMP算法一样,AC自动机在匹配时如果当前字符串匹配失败,那么利用失配指针进行跳转。由此可知如果跳转,跳转后的串的前缀必为跳转前的模式串的后缀,并且跳转的新位置的深度(匹配字符个数)一定小于跳之前的节点(跳转后匹配字符数不可能大于跳转前,否则无法保证跳转后的序列的前缀与跳转前的序列的后缀匹配)。
  • 因为是一个字典树(用ch数组表示,是一个二维数组,ch[u][c]),由于字典树c就为当前点++tot,u为上一个点,ch[u][c]就为当前点是否为树上的第几个点。 dp[ch[u][i]](等同于dp[i++] 由tire树ch[u][c]==++tot(将字典树上的某一个链看做需要匹配的字符串) ) =ch[dp[u]][i](等同于j++(上次匹配位置的++tot)) ;
  • 显然我们在构建失配数组的时候都是从当前节点的父节点的失配数组出发,由于Trie树将所有单词中相同前缀压缩在了一起,所以所有失配数组都不可能平级跳转(到达另一个与自己深度相同的节点),因为如果平级跳转,很显然跳转所到达的那个节点肯定不是当前匹配到的字符串的后缀的一部分,否则那两个节点会合为一个,所以跳转只能到达比当前深度小的节点,又是由当前节点(ch[u][c]==++tot )父节点(u )开始的跳转,所以这样就可以保证从root到所跳转到位置的那一段字符串长度小于当前匹配到的字符串长度。另一方面,我们可以类比KMP求NEXT数组时求最大匹配数量的思想,那种思想在AC自动机中的体现就是当构建失配数组时不断地回到之前的跳转位置,然后判断跳转位置的下一个字符是否包含当前字符,如果是就将失配数组与那个跳转位置连接,如果跳转位置指向tire树上的空点就说明当前匹配的字符在当前深度之前没有出现过,无法与任何跳转位置匹配,而若是找到了第一个跳转位置的下一个字符包含当前字符的的跳转位置,则必然取到了最大的长度,这是因为其余的当前正在匹配的字符必然在第一个跳转位置的下一个字符包含当前字符的的跳转位置深度之上,而那样的跳转位置就算可以,也不会是最大的(最后一个字符的深度比当前找到的第一个可行的跳转位置的最后一个字符的深度小,串必然更短一些)。
    if(!ch[u][i])/**如果不存在u的转移边i时,失配,KMP*/
                ch[u][i]=ch[dp[u]][i];///优化(即找到后缀完全相同且长度最大的位置)
  • 如图:
    在这里插入图片描述

查询操作(与字符串匹配)

void find(char *s)
{
    int u=1,len=strlen(s),c,k;
    for(int i=0; i<=len; i++)
    {
        c=s[i]-'a';
        k=ch[u][c];
        while(k>1)
        {
            ans+=bo[k];
            bo[k]=0;
            k=dp[k];
        }
        u=ch[u][c];
    }
    return ;
}
  • 如果节点匹配,就一直进行下去,每次都加每个节点的bo[u],并初始化为0,但只有代表单词结尾的字符bo[u]才是1,其他的为0.所以才有
   while(k>1)
        {
            ans+=bo[k];
            bo[k]=0;
            k=dp[k];
        }

例如,在查询过程中,匹配字符为abcdef,若有一个模式串为bc,那么因为KMP只扫过前缀,就会忽略导致结果错误,所以每次让某匹配点失配,看匹配串的后缀和之前匹配的前缀是否存在一模式串(bo[u]!=0)到空点结束.

  • 该查询默认一直匹配,因为在构造失配数组时进行了优化,见下代码
 if(!ch[u][i])/**如果不存在u的转移边i时,失配,KMP*/
                ch[u][i]=ch[dp[u]][i];///优化(即找到后缀完全相同且长度最大的位置)

导致一旦失配,就会有点进行补充(前面构造失配数组做的优化操作)。
直到需要查询的串走完。
(总的来说,算法比较神奇,但是可以直接套KMP模板,简单来说,失配数组的作用就是将主串某一位之前的所有可以与模式串匹配的单词快速在Trie树中找出。)

俗话说言多必失,更不要说我还是一个蒟蒻 QAQ 这么长的理解,一定有地方有些小失误,欢迎大犇来指正orz。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值