AC自动机--速成版

没办法,学了又忘了,暑假上课没听懂的锅现在要补......

AC自动机的思想:

Trie树 + KMP,但是有人称它为"树上KMP",我觉得也蛮形象的。

确实也是这样子的,AC自动机主要就是运用了KMP类似的思想以及操作:"失配指针"(字符串匹配都是运用已知信息来减少无效匹配次数......)

同时AC自动机支持多个模式串的匹配,这样子就运用到了Trie树。

(Fail)指针

(Fail)指针的含义:最长的 当前字符串的后缀 在Trie树上的编号

如果一个点(i)(Fail)指针指向(j)。那么根节点到(j)的字符串是根节点到(i)的字符串的一个后缀。
以下面的图为例,我们肉眼就可以发现,最左下的那个"d"的(Fail)指针应该指向中间最下面那个"d",因为"abcd"的后缀集就有"bcd"这一元素

具体实现((Fail指针的构建方法))

我们明确一点,我们是用Trie树上BFS的方式来建立(Fail)指针.

  • 首先我们把模式串都丢到Trie上面去

BMktED.png

(我们默认叶子节点是每个模式串的最后一个字符(真实情况未必是这样!))

ps.上面这张图借用的是luogu用户hicc0305

显然,每一个点(i)(Fail)指针指向的点的深度一定是比(i)小的(因为是后缀)。

同时,我们要使得(i)(Fail)指针指向的位置尽量的深。

分情况讨论:

  • 如果(i)的父亲节点不是真正的存在(i)这个子节点(也就是没有真实出现在模式串中,但是我们仍然要遍历)

也就是说比如串"abcb"

我们的第3个字符是c,在第三个字符这一层我们仍然要遍历"a , b , c , d , e , f , g "等等....直到"z",并且为它们建立(Fail)指针,但是显然只有"c"是真正在文本串中的。

那么对于例如上面的"a,b,d,e,f,g"等等都是并非"真实存在"的,就把当前节点的子节点指向(Fail_{fa})节点的具有相同的值的子节点。

  • 如果 (i)的父亲节点确确实实的存在(i)这个子节点(出现在上图中的样子,也就是确确实实出现在一个模式串中的)

不妨令(i)父亲(Fail)指针为(Fail_{fa})

(i)(Fail)指针指向的点为(Fail_i),那么我们就会令(Fail_i)等于(Fail_{fa})的儿子节点中与(i)值相同的那个点.

很多人没有介绍这里,蒟蒻我看了十分钟才明白为什么(Fail_{fa})的儿子节点中 一定 会有一个点与(i)值相同

首先(i)节点的父亲节点一定是深度比它浅的,那么父亲节点的(Fail)指针又比父亲节点浅。这样子(Fail_{fa})至少比(i)的深度少了2。

(Fail_{fa})的儿子节点中的与(i)值相同的节点至少深度比(i)少1,满足(Fail)指针的条件。

根据BFS,我们默认(i)上层的一定是已经构造好了(Fail)指针的了,而且根据构造方法,对于任何一个文本种类

我们都会遍历,就如同上面说的"abcb"的例子中,遍历第三层的时候我们不仅仅帮"c"建立了(Fail)指针,我们还帮"a,b,d,e,f,g...x,y,z"都建立了(Fail)指针,所以只要是深度比(i)小的节点中,必定有一个节点跟(i)的值相同!

对于构造方法的例子:

我们以图上最左下那个"d"为例,我们假设遍历到了这个节点,因为是BFS,我们默认它上层所有节点已经建成了

(Fail) 指针。这里它的父亲节点"c"的(Fail)指针显然应该指向最中间的"c"节点,那么我们根据构造方法,

(Fail_i) 等于(Fail_{fa})的儿子节点中与(i)值相同的那个点,也就是最中间的"c"的子节点"d"。

(Fail)指针的构建到此结束.按照上面两种情况即可建成(Fail)指针,代码也很好写呀!

康康代码吧

Code

#include <bits/stdc++.h>
using namespace std;
struct {
	int end,Fail;
	int son[26];
}AC[1000005];
char a[1000005],s[1000005];
int cnt = 0,n;
int vis[1000005];
void build()
{
	int len = strlen(s),now = 0;
	for(int i = 0 ; i < len ; i ++)
	{
		int num = s[i] - 'a';
		if(! AC[now].son[num])
			AC[now].son[num] =++cnt;
		now = AC[now].son[num];
	}
	AC[now].end ++;
}

void GetFail()
{
	int now = 0,head = 0,tail = 0;
	for(int i = 0 ; i < 26 ; i ++)
		if(AC[0].son[i] != 0)
			tail ++ , vis[tail] = AC[0].son[i];
	//第一层的Fail指针只能指向Root(这里是0号节点),优先进入队列
	while(head < tail)
	{
		head ++;
		int v = vis[head];
		int Fail = AC[v].Fail;//这个点的Fail指针 
		for(int i = 0 ; i < 26 ; i ++)//遍历当前点的儿子节点
		{
			if(AC[v].son[i])//如果"真实存在这个点"
			{
				AC[AC[v].son[i]].Fail = AC[Fail].son[i];
				tail ++;
				vis[tail] = AC[v].son[i];//记得入队
			}
			else AC[v].son[i] = AC[Fail].son[i];
			//否则就把这个儿子节点接到Fail[fa]的相同值的儿子节点上
		}
	}
	return ;//这就完事了......
}

void GetAns()
{
	int len = strlen(a),now = 0 , ans = 0;
	for(int i = 0 ; i < len  ; i ++)
	{
		int num = a[i] - 'a';
		now = AC[now].son[num];
		for(int u = now ; AC[u].end != -1 && u ; u = AC[u].Fail)
                  //u == 0 表示访问到了一个虚节点,那么匹配失败,AC[u] == -1表示当前已经匹配过了,就跳走
		{
			ans += AC[u].end;
			AC[u].end = -1;
		}
	}
	cout << ans << endl;
	return ;
}

int main()
{
	cin >> n;
	for(int i = 1 ; i <= n ; i ++)
	{
		cin >> s;
		build();
	}
	GetFail();
	cin >> a;
	GetAns();
	return 0;
}

AC自动机加强版:

给你n个模式串以及1个文本串,你需要求出哪个模式串在文本串中出现次数最多,输出最多出现的次数以及出现次数最多的模式串.

思路

修改一下end存的东西以及查询答案的方式就行了(char数组开小了居然没有RE而是输出超限?不知道输出了些啥(数组越界居然会影响这么多东西)......)

#include <bits/stdc++.h>
using namespace std;
int n;
int cnt = 0,coun[500],T = 0;
char s[1505];
char t[1500005];
char ans[155][75];
int vis[1000005];
#define Faili AC[AC[v].son[i]].Fail
struct {
	int end,Fail;
	int son[26];
	void clea()
	{
		end = Fail = 0;
		for(int i = 0 ; i < 26 ; i ++)
			son[i] = 0;
	}
}AC[1000005];

void build(int k)
{
	int len = strlen(s) , now  = 0;
	for(int i = 0 ; i < len ; i ++)
	{
		int num = s[i] - 'a';
		if(AC[now].son[num] == 0)
			cnt ++ , AC[now].son[num] = cnt;
		now = AC[now].son[num];
		ans[k][i] = s[i];
	}
	AC[now].end = k;
	return ;
}

void GetFail()
{
	int head = 0 , tail = 0 , now = 0;
	for(int i = 0 ; i < 26 ; i ++)
		if(AC[0].son[i])tail++,vis[tail] = AC[0].son[i];
	while(head < tail)
	{
		head++;
		int v = vis[head];
		int Failfa = AC[v].Fail;
		for(int i = 0 ; i < 26 ; i ++)
		{
			if(AC[v].son[i])
			{
				Faili = AC[Failfa].son[i];
				tail ++;
				vis[tail] = AC[v].son[i];
			}
			else AC[v].son[i] = AC[Failfa].son[i];
		}
	}
	return ;
}

void clean()
{
	for(int i = 0 ; i <= cnt ; i ++)
		AC[i].clea();
	for(int i = 1 ; i <= T ; i ++)
	{
		coun[i] = 0;
		memset(ans[i],0,sizeof(ans[i]));
	}
	cnt = 0;T = 0;
}

void Compare()
{
	int len = strlen(t) , now = 0;
	for(int i = 0 ; i < len  ;i ++)
	{
		int num = t[i] - 'a';
		now = AC[now].son[num];
		int u = now;
		for(; u ; u = AC[u].Fail)
		{
			coun[AC[u].end]++;
		}
	}
}

int main()
{
	while(1)
	{
		cin >> n;
		if(n == 0)break;
		clean();
		
		while(n)
		{
			cin >> s;
			T++;
			build(T);
			memcpy(ans[T],s,sizeof(s));
			n--;
		}
		
		GetFail();
		cin >> t;
		Compare();
		int M = 0;
		for(int i = 1 ; i <= T ; i ++)
			M = max(M,coun[i]);
		cout << M << endl;
		for(int i = 1 ; i <= T ; i ++)
			if(coun[i] == M)cout << ans[i] << endl;
	}
	return 0;
}
原文地址:https://www.cnblogs.com/MYCui/p/13882699.html