AC自动机

多重匹配求连续子串问题
AC自动机入门题
对于字符串匹配可以用kmp,但是对于多个字符串匹配呢
用ac自动机
也就是kmp+trie

大佬博客

ac自动机

模式串he,she,him,hers,shit构成的trie树

然后去查询fail指针

fail指针的理解:

是把下层的去指向上层
而对于上层的子串必定是下层的子串,所有说如果能匹配下层,必定能匹配上层(利用这个减少时间复杂度)

第一层(根下面的一层)肯定是指向根的
非第一层的,如果满足不了上述则也指向根,表示该子串和前面的子串没有公共前缀,要重新开始匹配

模板

传送门

  • 对子字符串进行构建trie树
  • 求失配指针
  • 用主串进行匹配
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int maxn = 1e6 + 5;
int cnt = 0;//trie的指针
struct tree{
    int fail;//失配指针
    int vis[26];//子结点的位置
    int end;//标记有几个单词以这个节点结尾
}ac[maxn];//trie树
void bulid(char *t){//对字典树进行初始化
    int len = strlen(t);
    int now = 0;//字典树当前的指针
    for(int i = 0; i < len; i++){
        if(ac[now].vis[t[i]-'a'] == 0){//trie树里没有这个子结点
            ac[now].vis[t[i]-'a'] = ++cnt;
        }
        now = ac[now].vis[t[i]-'a'];//向下构造
    }
    ac[now].end += 1;
}
void get_fail(){//构建fail指针
    queue<int>q;
    for(int i = 0; i < 26; i++){//对第一层的进行处理,全部指向根(第0层)
        if(ac[0].vis[i] != 0){
            ac[ac[0].vis[i]].fail = 0;
            q.push(ac[0].vis[i]);//同时把第一层的所有子结点压入队列里
        }
    }
    while(!q.empty()){
        int u = q.front();
        q.pop();
        for(int i = 0; i < 26; i++){
            if(ac[u].vis[i] != 0){//存在这个子结点
                ac[ac[u].vis[i]].fail = ac[ac[u].fail].vis[i];
                q.push(ac[u].vis[i]);
            }else{//不存在子结点
                ac[u].vis[i] = ac[ac[u].fail].vis[i];
            }
        }
    }
}
int ac_query(char *s){
    int len = strlen(s);
    int now = 0, ans = 0;
    for(int i = 0; i < len; i++){
        now = ac[now].vis[s[i] - 'a'];
        for(int t = now; t && ac[t].end != -1; t = ac[t].fail){
            ans += ac[t].end;//只有单词都匹配到,且匹配到结尾才会加非零数
            ac[t].end = -1;
        }
    }
    return ans;
}
int main(){
    int n;
    scanf("%d", &n);
    char s[maxn];//主串
    char t[maxn];//子串
    for(int i = 0; i < n; i++){
        scanf("%s", t);//子串
        bulid(t);
    }
    ac[0].fail = 0;//结束标记
    get_fail();
    scanf("%s", s);//主串
    printf("%d\n", ac_query(s));

    return 0;
}

优化版模板

  • 根的失配是根
  • 先把所有根没指向的字母的失配指针指向根---初始化
  • 根指向的字母的的下一个字母失配指向根---初始化,且把下一个字母放入队列
  • 循环操作,寻找失配指针
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int maxn=1e6+5;
struct ac_auto{
    int fail[maxn];//失配指针
    int vis[maxn][26];//子结点的位置
    int end[maxn];//标记有几个单词以这个节点结尾
    int L,root;//L是编号,root是根节点,root一直没变过,就是根的位置

    int newNode(){
        for(int i=0;i<26;i++){//26叉树
            vis[L][i]=-1;
        }
        end[L++]=0;
        return L-1;//返回节点编号
    }
    void initial(){
        L=0;
        root=newNode();//把第一行清空了,root=0,L=1
    }
    void Insert(char *t){
        int len=strlen(t);
        int now=root;
        for(int i=0;i<len;i++){
            int x=t[i]-'a';
            if(vis[now][x]==-1)vis[now][x]=newNode();//若子节点没有t[i],则插入t[i]
            now=vis[now][x];//这个前缀单词在的话,前往这个单词的位置
        }
        end[now]++;
    }
    void get_fail(){
        queue<int>q;
        fail[root]=root;//根指向根
        for(int i=0;i<26;i++){//对于根结点下的第一排字母(单词的首个字母)
            if(vis[root][i]==-1)vis[root][i]=root;//不存在这个字母,就指向根
            else{//存在这个字母
                fail[vis[root][i]]=root;//这个字母的下一个字母的失配指针指向根???
                q.push(vis[root][i]);//再把这个字母的下一个字母放进去
            }
        }

        while(!q.empty()){
            int now=q.front();
            q.pop();
            for(int i=0;i<26;i++){
                if(vis[now][i]==-1)vis[now][i]=vis[fail[now]][i];
                else{
                    fail[vis[now][i]]=vis[fail[now]][i];
                    q.push(vis[now][i]);
                }
            }
        }
    }

    int Query(char *s){
        int now=root;
        int ans=0;
        int len=strlen(s);

        for(int i=0;i<len;i++){
            now=vis[now][s[i]-'a'];
            int temp=now;
            while(temp!=root){
                ans+=end[temp];
                end[temp]=0;
                temp=fail[temp];
            }
        }
        return ans;
    }
}ac;
int t,n;
char s[maxn];
char str[maxn];
int main(){
    scanf("%d",&t);
    while(t--){
        scanf("%d",&n);
        ac.initial();
        for(int i=0;i<n;i++){
            scanf("%s",s);
            ac.Insert(s);
        }
        ac.get_fail();
        scanf("%s",s);
        printf("%d\n",ac.Query(s));
    }
}

题目

记录哪几个单词出现过

传送门

/*
和模板比,就加了个used来记录这个单词是否被记录过
且把end改成了记录结点而不是记录几次出现,(对于每一个字典树的点,只记录了唯一的一个数值)
同时把26个字母改成了128个
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int maxn=1e6+5;
struct ac_auto{
    int fail[maxn];//失配指针
    int vis[maxn][128];//子结点的位置
    int end[maxn];//标记有几个单词以这个节点结尾
    int L,root;//L是编号,root是根节点,root一直没变过,就是根的位置
    bool used[510];

    int newNode(){
        for(int i=0;i<128;i++){//26叉树
            vis[L][i]=-1;
        }
        end[L++]=0;
        return L-1;//返回节点编号
    }
    void initial(){
        L=0;
        root=newNode();//把第一行清空了,root=0,L=1
    }
    void Insert(char *t,int id){
        int len=strlen(t);
        int now=root;
        for(int i=0;i<len;i++){
            int x=t[i];//直接赋值为ASCII码
            if(vis[now][x]==-1)vis[now][x]=newNode();//若子节点没有t[i],则插入t[i]
            now=vis[now][x];//这个前缀单词在的话,前往这个单词的位置
        }
        end[now]=id;//now的值是唯一的

    }
    void get_fail(){
        queue<int>q;
        fail[root]=root;//根指向根
        for(int i=0;i<128;i++){//对于根结点下的第一排字母(单词的首个字母)
            if(vis[root][i]==-1)vis[root][i]=root;//不存在这个字母,就指向根
            else{//存在这个字母
                fail[vis[root][i]]=root;//这个字母的下一个字母的失配指针指向根???
                q.push(vis[root][i]);//再把这个字母的下一个字母放进去
            }
        }

        while(!q.empty()){
            int now=q.front();
            q.pop();
            for(int i=0;i<128;i++){
                if(vis[now][i]==-1)vis[now][i]=vis[fail[now]][i];
                else{
                    fail[vis[now][i]]=vis[fail[now]][i];
                    q.push(vis[now][i]);
                }
            }
        }
    }

    int Queue(char *s){
        memset(used,false,sizeof(used));
        int now=root;
        int ans=0;
        int len=strlen(s);

        for(int i=0;i<len;i++){
            int x = s[i];
            now=vis[now][x];
            int temp=now;
            while(temp!=root){
                if(end[temp]!=0){
                    used[end[temp]]=true;
                    ans =1;
                }
                temp=fail[temp];
            }
        }
        return ans;
    }
}ac;
int t,n;
char s[maxn];
char str[maxn];
int main(){
    int m;
    scanf("%d",&m);
    ac.initial();
    for(int i=1;i<=m;i++){
        scanf("%s",str);
        ac.Insert(str,i);
    }
    ac.get_fail();
    int n;
    cin>>n;
    int ans=0;
    for(int i=1;i<=n;i++){
        scanf("%s",s);
        if(ac.Queue(s)!=0){
            printf("web %d:",i);
            for(int i=1;i<=m;i++){
                if(ac.used[i]!=false){
                    printf(" %d",i);
                }
            }
            ans++;
            putchar('\n');
        }
    }
    printf("total: %d\n",ans);
}

每个单词出现的次数,可重叠

/*
和模板比
end改成了记录结点而不是记录几次出现,(对于每一个字典树的点,只记录了唯一的一个数值)
同时把26个字母改成了128个
而且把单词编号加入了Hash来寻找哪个单词,并且记录出现的次数
还有一个记录字符串的结构体
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int maxn=2e6+5;
int Hash[1005];
struct node{
    char ss[55];
}words[1005];
struct ac_auto{
    int fail[maxn];//失配指针
    int vis[maxn][128];//子结点的位置
    int end[maxn];//标记有几个单词以这个节点结尾
    int L,root;//L是编号,root是根节点,root一直没变过,就是根的位置

    int newNode(){
        for(int i=0;i<128;i++){//26叉树
            vis[L][i]=-1;
        }
        end[L++]=0;
        return L-1;//返回节点编号
    }
    void initial(){
        L=0;
        root=newNode();//把第一行清空了,root=0,L=1
    }
    void Insert(char *t,int id){
        int len=strlen(t);
        int now=root;
        for(int i=0;i<len;i++){
            int x=t[i];//直接赋值为ASCII码
            if(vis[now][x]==-1)vis[now][x]=newNode();//若子节点没有t[i],则插入t[i]
            now=vis[now][x];//这个前缀单词在的话,前往这个单词的位置
        }
        end[now]=id;

    }
    void get_fail(){
        queue<int>q;
        fail[root]=root;//根指向根
        for(int i=0;i<128;i++){//对于根结点下的第一排字母(单词的首个字母)
            if(vis[root][i]==-1)vis[root][i]=root;//不存在这个字母,就指向根
            else{//存在这个字母
                fail[vis[root][i]]=root;//这个字母的下一个字母的失配指针指向根???
                q.push(vis[root][i]);//再把这个字母的下一个字母放进去
            }
        }

        while(!q.empty()){
            int now=q.front();
            q.pop();
            for(int i=0;i<128;i++){
                if(vis[now][i]==-1)vis[now][i]=vis[fail[now]][i];
                else{
                    fail[vis[now][i]]=vis[fail[now]][i];
                    q.push(vis[now][i]);
                }
            }
        }
    }

    void Query(char *s){
        int now=root;
        int ans=0;
        int len=strlen(s);

        for(int i=0;i<len;i++){
            int x = s[i];
            now=vis[now][x];
            int temp=now;
            while(temp!=root){
                if(end[temp]!=0){
                    Hash[end[temp]]++;
                }
                temp=fail[temp];
            }
        }
    }
}ac;
int t,n;
char s[maxn];
int main(){
    int m;
    while(cin>>m){
        ac.initial();
        memset(Hash,0,sizeof(Hash));
        for(int i=1;i<=m;i++){
            scanf("%s",words[i].ss);
            ac.Insert(words[i].ss,i);
        }
        ac.get_fail();
        scanf("%s",s);
        ac.Query(s);
        for(int i=1;i<=m;i++){
            if(Hash[i]!=0){
                printf("%s: %d\n",words[i].ss,Hash[i]);
            }
        }
    }
}

posted @ 2019-09-18 17:57  Emcikem  阅读(210)  评论(0编辑  收藏  举报