zoukankan      html  css  js  c++  java
  • AC自动机入门和几道例题

    一直被AC自动机这个名字唬住,以为很难,自动AC?其实不是。

    AC自动机=字典树+KMP。字典树是必须要懂的;KMP主要了解一下回溯思想,问题不大。

    • KMP解决的是一个母串和一个模式串的匹配问题。
    • 字典树解决的是许多字符串的前缀和问题。
    • AC自动机解决的是一个母串和许多模式串的匹配问题,把所有的模式串搞成一棵字典树,再用母串去字典树上跑。

    引入失配指针的概念,对于当前遍历到的母串某个字符,在字典树中找不下去了,不从根开始,像KMP一样回溯到某个位置,而失配指针 指向的就是应该回溯的位置。

    1.失配指针如何构造?BFS

    根root当作第0层,root的fail指针指向null;往下数,特殊的第1层的fail指针都指向root层;对于第2层往下 的节点,假设当前遍历到的节点为now,now的儿子的失配指针 指向 now的失配指针指向的上层节点(假设为p)的儿子。例如下图

    root是第0层

    例1,当前遍历到的now为第1层的'n',now有个儿子为'x',now的失配指针指向root,root还有个右儿子叫'x',则now的儿子'x'(第2层左边的x)的失配指针 指向 root的右儿子'x'。

    例2,当前遍历到的now为第2层左边的'x',now的右儿子为'x'(第3层右边的'x'),now的失配指针指向的上层节点(第1层右边的'x')假设为p,p刚好有一个儿子'x'(第2层右边的'x')与now的儿子'x'相同,则now的右儿子'x'的失配指针 指向p的儿子'x'。 

    例3,当前遍历到的now为第2层左边的'x',now的左儿子为'n'(第3层左边的'n'),now的失配指针指向的上层节点(第1层右边的'x')假设为p,p没有儿子与now的儿子'n'相同,就再找p失配指针指向的root,root的左儿子'n',则now左儿子'n'的失配指针指向root的左儿子'n'。

    如果没有符合条件的节点,则指向root,表示失配的时候重新开始。

    这是一种公共前缀的思想,确定回溯到的位置,敲一两次就能够理解了。

    https://www.luogu.com.cn/problem/P3796

    在字典树上标记模式串,用母串跑字典树遇到终止节点就计数,找出最大,再用num判断最大值输出结果。

    #include<stdio.h>
    #include<iostream>
    #include<algorithm>
    #include<cstring>
    #include<math.h>
    #include<string>
    #include<map>
    #include<queue>
    #include<stack>
    #include<set>
    #include<ctime>
    #define ll long long
    #define inf 0x3f3f3f3f
    const double pi=3.1415926;
    using namespace std;
    
    struct node
    {
        int id;
        int flag;///判断是否单词在这里就完了,作为前缀
        node *next[27];
        node *fail;///失配指针
        node()   ///构造函数,创建的时候执行清0。不写也没关系,全局变量定义都是默认0
        {
            id=0;
            flag=0;
            fail=NULL;
            for(int i=0;i<26;i++)
                next[i]=NULL;
        }
    };
    int n;
    char a[155][77];
    int num[155];
    char s[1000005];
    node* root;
    int maxx=-inf;
    
    
    void insert(char * s,int id)
    {
        node* p=root;
        int len=strlen(s);
        for(int i=0;i<len;i++)
        {
            int x=s[i]-'a';
            if((p->next[x])==NULL)///当前遍历到的字母还没有开创节点
                p->next[x]=new node();///新开一个节点
            p=p->next[x];
        }
        p->flag++;///标记有单词在这里结束
        p->id=id;
    }
    
    
    void get_fail()
    {
        queue<node*>que;
        que.push(root);
        while(!que.empty())
        {
            node* now=que.front();///now是当前处理的节点,对now的儿子的fail进行处理
            que.pop();
            for(int i=0;i<26;i++)
            {
                if(now->next[i]!=NULL)///寻找非空子节点
                {
                    que.push(now->next[i]);
                    if(now==root)///特判根节点
                        now->next[i]->fail=root;///根节点的儿子即第一层的节点 的失配指针 指向根节点
                    else
                    {
                        node* p=now->fail;///p此时是now的失配指针,往上 指向 上层节点
                        while(p!=NULL)
                        {
                            if(p->next[i]!=NULL)///如果 上层节点p 有一个 和now的i儿子相同 的儿子
                            {
                                now->next[i]->fail=p->next[i];///就把(now的儿子的 失配指针) 指向 (now的失配指针指向的上层节点p的儿子)
                                break;
    
                            }
                            p=p->fail;///没有就不断回溯,最后会回溯到根节点root,再回溯到root的失配指针NULL
                        }
                        if(p==NULL)///此时已经回溯到NULL,走投无路了
                            now->next[i]->fail=root;
    
                    }
                }
            }
        }
    }
    
    
    
    void query(char *s)
    {
    
        node* p=root;///p为模式串指针
        int len=strlen(s);
        for(int i=0;i<len;i++)
        {
            int x=s[i]-'a';
            while(p->next[x]==NULL && p!=root)///匹配不到就找失配指针,如果是根节点就不允许找失配指针
                p=p->fail;
    
            p=p->next[x];
    
            if(p==NULL)///p如果是空的,即匹配不到,直接重新来过,也不满足接下来的while
                p=root;
    
            node* temp=p;
            while(temp!=root)
            {
                if( temp->flag >0 )
                {
                    num[ temp->id ]++;///计数
                    maxx=max(maxx,num[temp->id]);
                }
                temp=temp->fail;
            }
        }
    }
    
    
    int main()
    {
        while(scanf("%d",&n) && n)
        {
            root=new node();
            maxx=-inf;
            memset(a,0,sizeof(a));
            memset(num,0,sizeof(num));
            for(int i=1;i<=n;i++)
            {
                getchar();
                scanf("%s",a[i]);
                insert(a[i],i);
            }
            get_fail();
    
    
            getchar();
            scanf("%s",s);
            query(s);
            printf("%d
    ",maxx);
            for(int i=1;i<=n;i++)
            {
                if(num[i]==maxx)
                    printf("%s
    ",a[i]);
            }
        }
        return 0;
    }
    P3796

    http://acm.hdu.edu.cn/showproblem.php?pid=2222

    找出母串包含多少种子串,因为是找种类,所以找到末尾节点标记,下一次不再累计。

    #include<stdio.h>
    #include<iostream>
    #include<algorithm>
    #include<cstring>
    #include<math.h>
    #include<string>
    #include<map>
    #include<queue>
    #include<stack>
    #include<set>
    #include<ctime>
    #define ll long long
    #define inf 0x3f3f3f3f
    const double pi=3.1415926;
    using namespace std;
    
    struct node
    {
        int flag;///判断是否单词在这里就完了,作为前缀
        node *next[27];
        node *fail;///失配指针
        node()   ///构造函数,创建的时候执行清0。不写也没关系,全局变量定义都是默认0
        {
            flag=0;
            fail=NULL;
            for(int i=0;i<26;i++)
                next[i]=NULL;
        }
    };
    node* root;///根节点始终为空
    
    
    int n;
    char s[1000005];
    
    void insert(char* s)
    {
        node* p=root;
        int len=strlen(s);
        for(int i=0;i<len;i++)
        {
            int x=s[i]-'a';
            if((p->next[x])==NULL)///当前遍历到的字母还没有开创节点
                p->next[x]=new node();///新开一个节点
            p=p->next[x];
        }
        p->flag++;///标记有单词在这里结束
    }
    
    void get_fail()
    {
        queue<node*>que;
        que.push(root);
        while(!que.empty())
        {
            node* now=que.front();///now是当前处理的节点,对now的儿子的fail进行处理
            que.pop();
            for(int i=0;i<26;i++)
            {
                if(now->next[i]!=NULL)///寻找非空子节点
                {
                    que.push(now->next[i]);
                    if(now==root)///特判根节点
                        now->next[i]->fail=root;///根节点的儿子即第一层的节点 的失配指针 指向根节点
                    else
                    {
                        node* p=now->fail;///p此时是now的失配指针,往上 指向 上层节点
                        while(p!=NULL)
                        {
                            if(p->next[i]!=NULL)///如果 上层节点p 有一个 和now的i儿子相同 的儿子
                            {
                                now->next[i]->fail=p->next[i];///就把(now的儿子的 失配指针) 指向 (now的失配指针指向的上层节点p的儿子)
                                break;
                            }
                            p=p->fail;///没有就不断回溯,最后会回溯到根节点root,再回溯到root的失配指针NULL
                        }
                        if(p==NULL)///此时已经回溯到NULL,走投无路了
                            now->next[i]->fail=root;
                    }
                }
            }
        }
    }
    
    int query(char *s)
    {
        int res=0;
        node* p=root;///p为模式串指针
        int len=strlen(s);
        for(int i=0;i<len;i++)
        {
            int x=s[i]-'a';
            while(p->next[x]==NULL && p!=root)///匹配不到就找失配指针,如果是根节点就不允许找失配指针
                p=p->fail;
    
            p=p->next[x];
    
            if(p==NULL)///p如果是空的,即匹配不到,直接重新来过,也不满足接下来的while
                p=root;
    
            node* temp=p;
            while(temp!=root)
            {
                if(temp->flag >=0 )
                {
                    res=res+temp->flag;///本题是算种类,不是个数,某个模式串已经累计过了就不再匹配
                    temp->flag=-1;
                }
                else
                    break;///哪怕break了,p依旧在字典树上走下去
    
                temp=temp->fail;
            }
        }
        return res;
    }
    
    
    
    int main()///hdu2222
    {
        int t;
        scanf("%d",&t);
        while(t--)
        {
            root=new node();///每个测试样例对root清空
            scanf("%d",&n);
            for(int i=1;i<=n;i++)
            {
                getchar();
                scanf("%s",s);
                insert(s);
            }
            get_fail();
            getchar();
            scanf("%s",s);
            printf("%d
    ",query(s));
        }
        return 0;
    }
    hdu2222

    http://acm.hdu.edu.cn/showproblem.php?pid=2896

    RE后发现此题字符不仅仅是大小写字母,next数组用128大小,然后MTE,每次用new node()新开节点成数组节点多次利用,再次MTE,把next数组成95,仅用可见字符,再次MTE。简直吐血,原本用G++提交成C++提交直接AC,wtm...!

    可见字符:算上空格, 从32到126共95个可见字符;不算上空格则为94个。

    #include<stdio.h>
    #include<iostream>
    #include<algorithm>
    #include<cstring>
    #include<math.h>
    #include<string>
    #include<map>
    #include<queue>
    #include<stack>
    #include<set>
    #include<ctime>
    #define ll long long
    #define inf 0x3f3f3f3f
    const double pi=3.1415926;
    using namespace std;
    
    struct node
    {
        int id;
        int flag;///判断是否单词在这里就完了,作为前缀
        node *next[95];///95个可见字符
        node *fail;///失配指针
    };
    node b[100005];
    node* root;
    int n,m;
    int num;
    int cnt;
    char word[205];
    char s[10005];
    
    void init(int j)///清空节点
    {
        b[j].id=b[j].flag=0;
        b[j].fail=NULL;
        for(int i=0;i<95;i++)
            b[j].next[i]=NULL;
    }
    
    void insert(char * s,int id)
    {
        node* p=root;
        int len=strlen(s);
        for(int i=0;i<len;i++)
        {
            int x=(int)s[i]-32;
            if((p->next[x])==NULL)///当前遍历到的字母还没有开创节点
            {
                init(cnt);///清空原来的节点,多次利用,不然一次次new会爆内存
                p->next[x]=b+cnt;///指针指向数组
                cnt++;
            }
    
            p=p->next[x];
        }
        p->flag++;///标记有单词在这里结束
        p->id=id;
    }
    
    void get_fail()
    {
        queue<node*>que;
        que.push(root);
        while(!que.empty())
        {
            node* now=que.front();///now是当前处理的节点,对now的儿子的fail进行处理
            que.pop();
            for(int i=0;i<95;i++)
            {
                if(now->next[i]!=NULL)///寻找非空子节点
                {
                    que.push(now->next[i]);
                    if(now==root)///特判根节点
                        now->next[i]->fail=root;///根节点的儿子即第一层的节点 的失配指针 指向根节点
                    else
                    {
                        node* p=now->fail;///p此时是now的失配指针,往上 指向 上层节点
                        while(p!=NULL)
                        {
                            if(p->next[i]!=NULL)///如果 上层节点p 有一个 和now的i儿子相同 的儿子
                            {
                                now->next[i]->fail=p->next[i];///就把(now的儿子的 失配指针) 指向 (now的失配指针指向的上层节点p的儿子)
                                break;
                            }
                            p=p->fail;///没有就不断回溯,最后会回溯到根节点root,再回溯到root的失配指针NULL
                        }
                        if(p==NULL)///此时已经回溯到NULL,走投无路了
                            now->next[i]->fail=root;
                    }
                }
            }
        }
    }
    
    
    
    void query(char *s,int cnt)
    {
        set<int>se;
        node* p=root;///p为模式串指针
        int len=strlen(s);
        for(int i=0;i<len;i++)
        {
            int x=(int)s[i]-32;
            while(p->next[x]==NULL && p!=root)///匹配不到就找失配指针,如果是根节点就不允许找失配指针
                p=p->fail;
    
            p=p->next[x];
    
            if(p==NULL)///p如果是空的,即匹配不到,直接重新来过,也不满足接下来的while
                p=root;
    
            node* temp=p;
            while(temp!=root)
            {
                if( temp->flag >0 )
                    se.insert(temp->id);
                temp=temp->fail;
            }
        }
    
        if(!se.empty())///输出答案
        {
            num++;
            printf("web %d:",cnt);
            for(set<int>::iterator it=se.begin();it!=se.end();it++)
                printf(" %d",*it);
            printf("
    ");
        }
    }
    
    
    int main()
    {
        while(scanf("%d",&n)!=EOF)
        {
            root=new node();
            num=0;
            cnt=0;
            for(int i=1;i<=n;i++)
            {
                getchar();
                scanf("%s",word);
                insert(word,i);
            }
            get_fail();
            scanf("%d",&m);
            for(int i=1;i<=m;i++)
            {
                getchar();
                scanf("%s",s);
                query(s,i);
            }
            printf("total: %d
    ",num);
        }
        return 0;
    }
    hdu2896

    https://www.luogu.com.cn/problem/P5231

    尝试用数组模拟指针写AC自动机,数组第0行相当于根节点。将模式串构造出AC自动机,再用母串跑,标记母串走过的路径,接下来再用模式串去匹配,看看能匹配母串走过的路径的前缀是多少。

    #include<stdio.h>
    #include<iostream>
    #include<algorithm>
    #include<cstring>
    #include<math.h>
    #include<string>
    #include<map>
    #include<queue>
    #include<stack>
    #include<set>
    #include<ctime>
    #define ll long long
    #define inf 0x3f3f3f3f
    const double pi=3.1415926;
    using namespace std;
    
    int maxx=10000005;
    int n,m;
    char s[10000005];
    char a[100005][111];
    int ans[100005];
    map<char,int>mp;
    
    int ac[10000005][5];///用数组模拟字典树和AC自动机
    int fail[10000005];
    int flag[10000005];///标记主串走过
    int cnt;
    
    void insert(char *s,int k)
    {
        int p=0;
        int len=strlen(s);
        for(int i=0;i<len;i++)
        {
            int c=mp[s[i]];
            if( ac[p][c]==0 )///开创节点,即新开一行数组分配
                ac[p][c]=++cnt;
            p=ac[p][c];///跳到这一行
        }
    }
    
    void get_fail()///构造失配指针
    {
        queue<int>que;
        for(int i=1;i<=4;i++)
            if(ac[0][i]!=0)
            que.push(ac[0][i]);
        while(!que.empty())
        {
            int now=que.front();
            que.pop();
            for(int i=1;i<=4;i++)
            {
                if(ac[now][i]!=0)
                {
                    fail[ ac[now][i] ]=ac[ fail[now] ][i] ;
                    ///now是当前行数,总会有一个下标不为空,表示一个字母是后续。这个后续的失配指针 是 当前行数的失配指针的儿子
                    ///最开始的失配指针总是指向0,相当于是根节点。
                    que.push(ac[now][i]);
                }
                else
                    ac[now][i]=ac[ fail[now] ][i];///如果匹配不到就 指向失配指针节点的儿子。如果失配指针节点是0,那就是重新开始
            }
        }
    
    }
    
    void query(char* s)
    {
        int p=0;///主串在ac自动机上跑的指针
        int len=strlen(s);
        for(int i=0;i<len;i++)
        {
            int c=mp[s[i]];
            p=ac[p][c];
            int j=p;///临时指针
            while(j!=0)///如果到了当前节点还有后续,就将当前节点的失配指针节点都标记
            {
                if(flag[j])
                    break;
                flag[j]=1;///标记这个点走过
                j=fail[j];
            }
        }
    }
    
    
    int find_ans(char* s)
    {
        int p=0,res=0;
        int len=strlen(s);
        for(int i=0;i<len;i++)
        {
            int c=mp[s[i]];
            p=ac[p][c];
            if(flag[p])
                res=i+1;
        }
        return res;
    }
    
    int main()///P5231
    {
        mp['E']=1;
        mp['S']=2;
        mp['W']=3;
        mp['N']=4;
        while(scanf("%d%d",&n,&m)!=EOF)
        {
            cnt=0;
            memset(ac,0,sizeof(ac));
            memset(fail,0,sizeof(fail));
            memset(flag,0,sizeof(flag));
            memset(ans,0,sizeof(ans));
            getchar();
            scanf("%s",s);
            for(int i=1;i<=m;i++)
            {
                getchar();
                scanf("%s",a[i]);
                insert(a[i],i);
            }
            get_fail();
            query(s);
            for(int i=1;i<=m;i++)
                printf("%d
    ",find_ans(a[i]));
        }
        return 0;
    }
    P5231
  • 相关阅读:
    python简单接口的测试(随机数等)
    关于数据库的去重+导入导出参数
    找到并杀死一个软件开启的进程
    blinker库
    HTTP状态码
    一致性哈希算法
    celery
    项目部署
    redis更多
    functools模块
  • 原文地址:https://www.cnblogs.com/shoulinniao/p/12110143.html
Copyright © 2011-2022 走看看