『AC自动机』基础算法理解与模板

AC自动机的fail数组

我们知道KMP算法就是两个字符串在进行模式匹配,而AC自动机则是一堆字符串和一个字符串进行的模式匹配。由于一堆字符串比较多,我们需要使用trie树来存储这些字符串。

AC自动机 f a i l fail fail数组,对于节点 x x x而言, f a i l x fail_x failx表示以 x x x结尾的后缀,和以根节点开头的前缀,最大的公共部分长度是多少。可KMP一样,我们可以使用递推的方法来进行求解。

对于当前的节点 u u u,以及在 t r i e trie trie数上的子节点 v v v而言:

  • 如果存在子节点为 v v v,可以将 u u u f a i l fail fail的子节点与v进行匹配,即令 v v v c [ f a i l u ] [ v ] c[fail_u][v] c[failu][v]进行匹配。此时我们思考 w = c [ f a i l u ] [ v ] w=c[fail_u][v] w=c[failu][v]的取值,如果存在,那肯定能匹配上;如果不存在,则我们可以让它指向一个 c [ f a i l w ] [ v ] c[fail_w][v] c[failw][v],则这样保证了不断跳fail一定匹配上或者指向根节点了。
  • 不存在,直接令 c [ u ] [ v ] = c [ f a i l u ] [ v ] . c[u][v]=c[fail_u][v]. c[u][v]=c[failu][v].理由在上面已经提到。

具体的实现,我们需要利用到广度优先遍历。

代码如下:

void build(string s,int num)
{
	int t = 0, len = s.size();
	for (int i=0;i<len;++i)
	{
		if (!c[t][s[i]-'a'])
		    c[t][s[i]-'a'] = ++Cnt;
		t = c[t][s[i]-'a'];
	}
	return;
}
//在trie数中插入字符串
void work(void)
{
	queue <int> q;
	for (int i=0;i<26;++i)
	    if (c[0][i]) {
	    	q.push(c[0][i]);
	    	fail[c[0][i]] = 0;
	    }
	while (q.size())
	{
		int t = q.front(); q.pop();
		for (int i=0;i<26;++i) {
			if (c[t][i]) fail[c[t][i]] = c[fail[t]][i], q.push(c[t][i]);
			else c[t][i] = c[fail[t]][i];
		}
	}
	return;
}
//广度优先遍历求解fail数组

模板1

给定 n n n个模式串和 1 1 1个文本串,求有多少个模式串在文本串里出现过。

我们每一次在插入操作结束以后用一个 e n d end end记录当前节点有几个结尾的。

在跳 f a i l fail fail的时候,每一次 f a i fai fail只能跳一遍,因为问的是由多少个模板串出现过。

注意跳 f a i l fail fail的实现方式。

#include <bits/stdc++.h>

using namespace std;
const int N = 2000000;

int n, cnt = 0;
int c[N][26], end[N], fail[N];

void build(string s)
{
	int t = 0 ,len = s.size();
	for (int i=0;i<len;++i)
	{
		if (c[t][s[i]-'a'] == 0)
			c[t][s[i]-'a'] = ++cnt;
		t = c[t][s[i]-'a'];
	}
	end[t] ++;
	return;
}

void work(void)
{
	queue <int> q;
	for (int i=0;i<26;++i) 
		if (c[0][i] > 0) 
			q.push(c[0][i]), fail[c[0][i]] = 0;
	while (q.size())
	{
		int t = q.front(); q.pop();
		for (int i=0;i<26;++i) {
			if (c[t][i] > 0) 
				q.push(c[t][i]), fail[c[t][i]] = c[fail[t]][i];
			else c[t][i] = c[fail[t]][i];
		}
	}
	return;
}

int ask(string s)
{
	int now = 0, ans = 0, len = s.size();
	for (int i=0;i<len;++i)
	{
		now = c[now][s[i]-'a'];
		for (int t = now; t > 0 && end[t] ^ -1; t = fail[t]) 
			ans += end[t], end[t] = -1;
	}
	return ans;
}

int main(void)
{
	cin>>n; 
	for (int i=1;i<=n;++i) {
		string S; cin>>S;
		build(S);
	}
	work();
	string S; cin>>S;
	cout<<ask(S)<<endl;
	return 0;
}

模板2

在这里插入图片描述我们每一次统计每一个数字的出现次数,在统计这个东东的时候不用将 e n d end end赋值为 1 1 1,因为统计的是出现次数而不是由多少个字符串。

我们用 e n d end end记录字符串标号,在跳 f a i l fail fail的使用用 c n t [ e n d [ t ] ] + + cnt[end[t]]++ cnt[end[t]]++即可。

#include <bits/stdc++.h>

using namespace std;
const int N = 20000;

int n, Cnt = 0;
string s[N];
int c[N][26], fail[N], end[N], cnt[N];

void Clear(void)
{
	n = Cnt = 0;
	memset(c,0,sizeof c);
	memset(end,0,sizeof end);
	memset(cnt,0,sizeof cnt);
	memset(fail,0,sizeof fail);
}

void build(string s,int num)
{
	int t = 0, len = s.size();
	for (int i=0;i<len;++i)
	{
		if (!c[t][s[i]-'a'])
		    c[t][s[i]-'a'] = ++Cnt;
		t = c[t][s[i]-'a'];
	}
	end[t] = num;
	return;
}

void work(void)
{
	queue <int> q;
	for (int i=0;i<26;++i)
	    if (c[0][i]) {
	    	q.push(c[0][i]);
	    	fail[c[0][i]] = 0;
	    }
	while (q.size())
	{
		int t = q.front(); q.pop();
		for (int i=0;i<26;++i) {
			if (c[t][i]) fail[c[t][i]] = c[fail[t]][i], q.push(c[t][i]);
			else c[t][i] = c[fail[t]][i];
		}
	}
	return;
}

void ask(string s)
{
	int now = 0, len = s.size();
	for (int i=0;i<len;++i)
	{
		now = c[now][s[i]-'a'];
		for (int t=now;t;t=fail[t])
		    cnt[end[t]] ++;
	}
	return;
}

void get_ans(void)
{
	int Max = 0;
	for (int i=1;i<=n;++i)
	    Max = max(Max,cnt[i]);
	cout<<Max<<endl;
	for (int i=1;i<=n;++i)
	    if (cnt[i] == Max) 
	    	cout<<s[i]<<endl;
	return;
}

void solve(void)
{
	cin>>n;
	if (n == 0) exit(0);
	for (int i=1;i<=n;++i) cin>>s[i];
	for (int i=1;i<=n;++i) build(s[i],i);
	work();
	cin>>s[0]; ask(s[0]);
	get_ans();
	return;
}

int main(void)
{
	while (1) Clear(), solve();
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值