AC自动机

(update on 2023.6.20)

前言:额,这个东西,难度有点超乎想象(noi级算法果然不是这么好学)(境界还是没达到),先沉淀一下吧,八月再咕(坐等八月update)


简介:在字典树上建立 fail 指针形成的一棵树,主要用于查询多字符串出现次数问题

首先是三个模板题:

最基础的模板题。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inf 0x7fffffff
const ll maxn=1000010;   
struct node{ll son[27],fail,flag;}trie[maxn];        
ll n,tot=1,ans;      
char s[maxn];     
inline void insert(char *s,ll root) {
	ll len=strlen(s+1);
	for (ll i=1;i<=len;++i) {
		ll lett=s[i]-'a';
		if (!trie[root].son[lett]) trie[root].son[lett]=++tot;
		root=trie[root].son[lett];
	}
	trie[root].flag++;
}  
inline void getfail() {
	queue<ll> q;
	for (ll i=0;i<26;++i) trie[0].son[i]=1;
	q.push(1);
	trie[1].fail=0;
	while (!q.empty()) {
		ll u=q.front();
		q.pop();
		for (ll i=0;i<26;++i) {
			ll v=trie[u].son[i],Fail=trie[u].fail;
			if (!v) {
				trie[u].son[i]=trie[Fail].son[i];
				continue;
			}
			trie[v].fail=trie[Fail].son[i];
			q.push(v);
		}
	}
}   
inline ll query(char *s,ll root) {
	ll len=strlen(s+1);
	for (ll i=1;i<=len;++i) {
		ll v=s[i]-'a';
		ll now=trie[root].son[v];//注意这里是用now来跳而不是用root
        //trie[now].flag!=-1:加这个东西的原因是题目要求的是有多少个模式串出现,所以为优化复杂度加的
		while (now>1&&trie[now].flag!=-1) {
			ans+=trie[now].flag;
			trie[now].flag=-1;//这个东西也是优化 
            now=trie[now].fail;
		}
		root=trie[root].son[v];
	}
	return ans;
}                                                                            
inline ll in() {
    char a=getchar();
	ll t=0,f=1;
	while(a<'0'||a>'9') {if (a=='-') f=-1;a=getchar();}
    while(a>='0'&&a<='9') {t=(t<<1)+(t<<3)+a-'0';a=getchar();}
    return t*f;
}
signed main() {
	n=in();
	for (ll i=1;i<=n;++i) {
		scanf("%s",s+1);
		insert(s,1);
	}
	getfail();
	scanf("%s",s+1);
	printf("%lld",query(s,1));
	return 0;
}


#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inf 0x7fffffff
const ll maxn=1000010;   
struct node{ll son[27],fail,flag,id;}trie[100010];        
ll n,tot=1,ans; 
ll each[155];     
char s[160][77];
inline void insert(char *s,ll root,ll num) {
	ll len=strlen(s+1);
	for (ll i=1;i<=len;++i) {
		ll lett=s[i]-'a';
		if (!trie[root].son[lett]) trie[root].son[lett]=++tot;
		root=trie[root].son[lett];
	}
	trie[root].flag++;
	if (trie[root].flag==1) trie[root].id=num;
	//与模板一不一样的地方就是这里记录了一下id 
}  
inline void getfail() {
	queue<ll> q;
	for (ll i=0;i<26;++i) trie[0].son[i]=1;
	q.push(1);
	trie[1].fail=0;
	while (!q.empty()) {
		ll u=q.front();
		q.pop();
		for (ll i=0;i<26;++i) {
			ll v=trie[u].son[i],Fail=trie[u].fail;
			if (!v) {
				trie[u].son[i]=trie[Fail].son[i];
				continue;
			}
			trie[v].fail=trie[Fail].son[i];
			q.push(v);
		}
	}
}   
inline ll query(char *s,ll root) {
	ll len=strlen(s+1);
	for (ll i=1;i<=len;++i) {
		ll v=s[i]-'a';
		ll now=trie[root].son[v];
		// 这个时候不再像模板一一样用flag去优化,因为每个模式串可能出现多次
		//若想模板一一样写的话不能保证统计上每一次出现情况 
		while (now>1) {
			if (trie[now].flag) each[trie[now].id]+=trie[now].flag;
			now=trie[now].fail;
		}
		root=trie[root].son[v];
	}
	return ans;
}  
inline void init() {
	memset(each,0,sizeof(each));
	memset(trie,0,sizeof(trie));
	tot=1,ans=0;
}                                                                          
inline ll in() {
    char a=getchar();
	ll t=0,f=1;
	while(a<'0'||a>'9') {if (a=='-') f=-1;a=getchar();}
    while(a>='0'&&a<='9') {t=(t<<1)+(t<<3)+a-'0';a=getchar();}
    return t*f;
}
signed main() {
	while (1) {
		n=in();
		if (!n) break;
		init();
		for (ll i=1;i<=n;++i) {
			scanf("%s",s[i]+1);
			insert(s[i],1,i);
		}
		getfail();
		char g[maxn];
		scanf("%s",g+1);
		query(g,1);
		for (ll i=1;i<=n;++i) ans=max(each[i],ans);
		printf("%lld\n",ans);
		for (ll i=1;i<=n;++i) if (each[i]==ans) printf("%s\n",s[i]+1);
	}
	return 0;
}

因为简单版我们在 query 的时候是一级一级暴力向上跳的,这样会导致浪费很多的复杂度(因为 fail 关系是一样且固定的,每次从一个节点上向上跳的时候都是只会跳固定的点)

观察到每个节点和他的 fail 满足类似父子关系,经过每个结点之后,因为我们要往上跳,一定会经过他的 fail,再经过他的 fail 的 fail。这产生了一种先后关系,所以我们为了满足这个先后关系可以将每个节点的 fail 节点建有向边,之后再跑拓扑排序。于是我们可以在每个 query 到的每个节点上打标记,在拓扑的时候把后到达的节点的权值加上先到达的结点的标记,这个节点新的标记值就是他的权值。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inf 0x7fffffff
const ll maxn=2000010;   
struct node{ll son[27],fail,flag,id,ans;}trie[maxn];        
ll n,tot=1,ans; 
ll each[2000010],du[maxn];     
string s[2000010];
unordered_map<string,ll> Hash;
vector<ll> G[maxn];
inline void insert(string s,ll root,ll num) {
	ll len=s.size();
	for (ll i=0;i<len;++i) {
		ll lett=s[i]-'a';
		if (!trie[root].son[lett]) trie[root].son[lett]=++tot;
		root=trie[root].son[lett];
	}
	trie[root].flag=1;
	trie[root].id=num;
}  
inline void getfail() {
	queue<ll> q;
	for (ll i=0;i<26;++i) trie[0].son[i]=1;
	q.push(1);
	trie[1].fail=0;
	while (!q.empty()) {
		ll u=q.front();
		q.pop();
		for (ll i=0;i<26;++i) {
			ll v=trie[u].son[i],Fail=trie[u].fail;
			if (!v) {
				trie[u].son[i]=trie[Fail].son[i];
				continue;
			}
			trie[v].fail=trie[Fail].son[i];
			du[trie[v].fail]++;
			q.push(v);
		}
	}
}   
inline void topo() {
	queue<ll> q;
	for (ll i=1;i<=tot;++i) if (!du[i]) q.push(i);
	while (!q.empty())   {
		ll now=q.front();
		q.pop();
		if (trie[now].flag) each[trie[now].id]+=trie[now].ans;
		ll v=trie[now].fail;
		trie[v].ans+=trie[now].ans;
		du[v]--;
		if (!du[v]) q.push(v);
	}
}
inline ll query(char *s,ll root) {
	ll len=strlen(s+1);
	for (ll i=1;i<=len;++i) {
		ll lett=s[i]-'a';
		root=trie[root].son[lett];
		trie[root].ans++;
	}
}                                                                        
inline ll in() {
    char a=getchar();
	ll t=0,f=1;
	while(a<'0'||a>'9') {if (a=='-') f=-1;a=getchar();}
    while(a>='0'&&a<='9') {t=(t<<1)+(t<<3)+a-'0';a=getchar();}
    return t*f;
}
signed main() {
	n=in();
	for (ll i=1;i<=n;++i) {
		cin>>s[i];
		insert(s[i],1,i);
	}
	getfail();
	char g[maxn];
	scanf("%s",g+1);
	query(g,1);
	topo();
	for (ll i=1;i<=n;++i) Hash[s[i]]=max(each[i],Hash[s[i]]);
	for (ll i=1;i<=n;++i) printf("%lld\n",Hash[s[i]]);
	return 0;
}

ACAM的一些性质:

  • 每个节点的 fail 节点为末尾的字符串一定是他的后缀

应用典例:You Are Given Some Strings...

考虑在文本串上枚举每一个中间点作为 \(s_i\)\(s_j\) 的分界点,所以 \(s_i\) 就是文本串分界点左边(以下简称左文本串)的后缀,\(s_j\) 就是文本串分界点右边(以下简称右文本串)的前缀。因为 ACAM 每个节点的 fail 节点为末尾的字符串一定是他的后缀,所以我们可以在从左到右进行文本串查询操作的时候,把当前枚举到的作为左右分界点,于是当前节点能跳 fail 的次数就是当前分界点情况下的后缀个数即 \(s_i\) 个数。

处理出每个分界点情况下的 \(s_i\) 个数之后,因为我们接下来要求的是前缀,所以我们再反向建一次 ACAM,再和求后缀的流程一样再跑一次即可。


刷题笔记:

  • ACAM + dfs

Word Puzzles

题目中没有给出固定的文本串,所以我们在给出的文本矩阵上从边界处开始 dfs,将 dfs 到的作为当前文本串的一个字符,边 dfs 边匹配。

  • ACAM + 栈

[USACO15FEB]Censoring G

[USACO15FEB] Censoring S原理一样

inline ll query(char *s,ll root) {
	ll len=strlen(s+1);
	for (ll i=1;i<=len;++i) {
		ll v=s[i]-'a';
		ll root=trie[root].son[v];
		
		st[++top]=i;
		pos[top]=root;
		if (trie[root].flag) {
			top-=trie[root].flag;
			if (top<0) root=0;
			else root=pos[top];
		}
// 		now=trie[now].fail;
	}
	
} 
  • ACAM 上搜索

[POI2000] 病毒

  • ACAM + dp

模板题:[JSOI2007]文本生成器

ACAM 的 dp 一般是有固定套路的,

大部分 \(f[i][j]\) 表示当前在节点 \(j\),且串长为i时的情况,

有时再加一维表示这个状态里面包含了哪些东西

而且 AC自动机 的 DP 会经常让你用矩阵乘法优化

posted @ 2023-06-20 16:50  Pwtking  阅读(37)  评论(0)    收藏  举报