AC 自动机

AC 自动机

Part I 何为 AC 自动机#

AC 自动机能够解决多个字符串 si 在某个字符串 t 匹配的问题

其算法过程如下:

  • 把所有的 si 建立一棵 trie

  • 求出每个点的 fail 编号

  • ttrie 树中搞匹配

总而言之,AC 自动机的核心在于建 trie 加求出 fail 的过程

fail 指针的定义,求法以及作用#

fail 的定义#

faili 的定义是:与以 i 节点为结尾的串的后缀有最大公共长度的前缀的结尾编号

画个图更好理解

比如说我们已经插入了字符串 FG , HERS , HIS , SHEtrie 树中,那么 trie 就长这样

其中实线箭头表示字符串中的字符,虚线箭头表示某个点 i 连向 faili

就例如字符串 SHE 中的 EfailHERS 中的 E,因为 SHE 中以 E 结尾的后缀 HEHERS 中出现过,且容易发现 HE 已经是最长的

如果点 i 没有这样的匹配的话那么 ifail 就为 0 (根)


fail 的求法#

首先我们可以确定,每一个点 ifail 指针指向的点的深度一定是比 i 小的(fail 是后缀 = 前缀,前缀必须从头开始匹配)

第一层的 fail 一定指的是根

i 的父亲 fafail 指针指的是 failfa,那么如果 failfa 有和 i 值相同的儿子 j,那么 ifail 就指向 j

由于我们在处理 i 的情况必须要先处理好 fa 的情况,也就是说 trie 树中的所有节点的 fail 必须按深度从小到大求,所以求 fail 我们使用 bfs 来实现

实现的一些细节:

  1. 如果不存在一个节点 i,那么我们可以将那个节点设为failfa的值和 i 相同的儿子,方便跳 fail

  2. 无论failfa存不存在和 i 值相同的儿子 j,我们都可以将 ifail 指向 j (因为在处理 i 的时候 j 已经处理好了,如果出现这种情况,j 的值是第 1 种情况,也是有实际值的)

inline void get_fail(){
    queue <int> q;
    for(int i=0;i<26;++i){
        int x=c[0][i];
        fail[x]=0;//第一层节点fail指向根
        if(x) q.push(x);
    }
    while(!q.empty()){
        int x=q.front();
        q.pop();
        for(int i=0;i<26;++i){
            if(c[x][i]){
                fail[c[x][i]]=c[fail[x]][i];//细节2
                q.push(c[x][i]);
            }
            else
                c[x][i]=c[fail[x]][i];//细节1
        }
    }
}

fail 的运用#

类似于 KMPnext 数组,fail 指针的一大运用就是可以通过让 t 不断在 trie 树上跳 fail 指针从而找到匹配

inline int query(int n,char *A){
    int rt=0,res=0;
    for(int i=0;i<n;++i){
        rt=c[rt][A[i]-'a'];
        for(int tmp=rt;tmp&&vis[tmp]!=-1;tmp=fail[tmp]){
            res+=vis[tmp];
            vis[tmp]=-1;
        }
    }
    return res;
}

当然,这只是 fail 指针最为基础的运用,后面还会有许多 fail 的妙用

P3808 【模板】AC 自动机(简单版)#

#include<bits/stdc++.h>
using namespace std;

const int N=1e6+5;

int cnt;
int c[N][26],vis[N];
int fail[N];

inline void insert(int n,char *A){
    int rt=0;
    for(int i=0;i<n;++i){
        if(!c[rt][A[i]-'a'])
            c[rt][A[i]-'a']=++cnt;
        rt=c[rt][A[i]-'a'];
    }
    ++vis[rt];
}

inline void get_fail(){
    queue <int> q;
    for(int i=0;i<26;++i){
        int x=c[0][i];
        if(x) q.push(x);
    }
    while(!q.empty()){
        int x=q.front();
        q.pop();
        for(int i=0;i<26;++i){
            if(c[x][i]){
                fail[c[x][i]]=c[fail[x]][i];
                q.push(c[x][i]);
            }
            else
                c[x][i]=c[fail[x]][i];
        }
    }
}

inline int query(int n,char *A){
    int rt=0,res=0;
    for(int i=0;i<n;++i){
        rt=c[rt][A[i]-'a'];
        for(int tmp=rt;tmp&&vis[tmp]!=-1;tmp=fail[tmp]){
            res+=vis[tmp];
            vis[tmp]=-1;
        }
    }
    return res;
}

int n;
char A[N];

signed main(){
    cin>>n;
    while(n--){
        scanf("%s",A);
        insert(strlen(A),A);
    }
    get_fail();
    scanf("%s",A);
    cout<<query(strlen(A),A);
}

Part II fail 指针的其他用法#

其实 fail 指针的用法不止于此

因为 fail 能从某个匹配的地方跳到另一个,所以就有了在建好 failtrie 树上的 dp

习题#

P3041 Video Game G#

我们先把所有的串插到 trie 树里面并跑出 fail 指针

然后考虑 trie 树中每个点的价值

显然 trie 中每个点的价值等于以它本身结尾的字符串个数加上以它的所有可以跳到的 fail 结尾的字符串个数

这个东西可以在算 fail 的时候顺带计算出来

我们设 trie 上点 i 的贡献为 vi,点 i 的子节点为 ch13

然后想想如何 dp

我们设 fi,j 表示已经记录了前 i 个字符,且当前节点在 trie 树中编号为 j 的最大得分

那么就可以得到:

fi,chj=max{fi1,j+vfailchj}

code

#include<bits/stdc++.h>
using namespace std;

const int N=1005;

inline int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
	while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return x*f;
}

struct trie{
	int c[3];
	int v,f;
}t[N*5];
int cnt;

inline void cmax(int &a,int b){if(a<b) a=b;}

namespace AC{
	inline void insert(int n,char *A){
		int rt=0;
		for(int i=0;i<n;++i){
			int ch=A[i]-'A';
			if(!t[rt].c[ch]) t[rt].c[ch]=++cnt;
			rt=t[rt].c[ch];
		}
		++t[rt].v;
	}
	inline void get_fail(){
		queue <int> q;
		for(int i=0;i<3;++i)
			if(t[0].c[i]){
				q.push(t[0].c[i]);
				t[0].f=0;
			}
		while(!q.empty()){
			int x=q.front();
			q.pop();
			for(int i=0;i<3;++i){
				int y=t[x].c[i];
				if(y){
					t[y].f=t[t[x].f].c[i];
					q.push(y);
				}
				else t[x].c[i]=t[t[x].f].c[i];
			}
			t[x].v+=t[t[x].f].v;
		}
	}
}


int n,k,ans;
char s[N];
int f[N][305];

signed main(){
	n=read(),k=read();
	for(int i=1;i<=n;++i){
		scanf("%s",s);
		AC::insert(strlen(s),s);
	}
	AC::get_fail();
	for(int i=0;i<=k;++i)
		for(int j=1;j<=cnt;++j)
			f[i][j]=-114514;
	for(int i=1;i<=k;++i)
		for(int j=0;j<=cnt;++j)
			for(int ch=0;ch<3;++ch)
				cmax(f[i][t[j].c[ch]],f[i-1][j]+t[t[j].c[ch]].v);
	for(int i=0;i<=cnt;++i) cmax(ans,f[k][i]);
	printf("%d",ans);
}

作者:Into_qwq

出处:https://www.cnblogs.com/into-qwq/p/16516938.html

版权:本作品采用「qwq」许可协议进行许可。

posted @   Into_qwq  阅读(28)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
more_horiz
keyboard_arrow_up light_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示