AC自动机-详解AC自动机以及模板

AC自动机算法简介

首先简要介绍一下AC自动机,英文名:Aho-Corasick automation,该算法在1975年产生于贝尔实验室,是著名的多模板匹配算法之一。一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过
要搞懂AC自动机,先要有字典树Trie和KMP模式匹配算法的基础知识。其中,KMP是用于一对一的字符串匹配,而trie虽然能用于多模式匹配,但是每次匹配失败都需要进行回溯,如果模式串很长的话会很浪费时间,所以AC自动机应运而生,如同Manacher一样,AC自动机利用某些操作阻止了模式串匹配阶段的回溯,将时间复杂度优化到了O ( n ) - n 为文本长度

AC自动机算法大致流程

1.构造一棵Trie,作为AC自动机的搜索数据结构。

2.构造fail指针,使当前字符失配时跳转到具有最长公共前后缀的字符继续匹配。如同 KMP算法一样, AC自动机在匹配时如果当前字符匹配失败,那么利用fail指针进行跳转。由此可知如果跳转,跳转后的串的前缀,必为跳转前的模式串的后缀并且跳转的新位置的深度(匹配字符个数)一定小于跳之前的节点。所以我们可以利用 bfs在 Trie上面进行 fail指针的求解。

3.扫描主串进行匹配。

AC自动机详细图解

对于AC自动机算法,要是只通过语言来描述,真的十分不容易理解,小编在这里就用图来详细解释一下这个算法啦。

1、首先给定模式串 “ash” , “shex” , “bcd” , “sha” ,然后我们根据模式串建立如下 Trie 树:
Trie
2、然后我们再了解下一步:,AC自动机就是在Trie树的基础上,增加一个fail指针,如果当前点匹配失败,则将指针转移到fail指针指向的地方,这样就不用回溯,而可以继续匹配下去了.(当前模式串后缀和fail指针指向的模式串部分前缀相同,如 abce bcd,我们找到 c 发现下一个要找的不是 e,就跳到 bcd 中的 c 处,看看此处的下一个字符 d 是不是应该找的那一个)。一般,fail 指针的构建都是用 bfs 实现的。
(1)首先每个模式串的首字母肯定是指向根节点的:
在这里插入图片描述
(2)现在第一层 bfs 遍历完了,开始第二层(根节点为第0层)第二层节点 s 是第一层节点 a 的子节点,但是我们还是要从 a - z 遍历,如果不存在这个子节点我们就让他指向根节点(如下图中的红色 a ):
在这里插入图片描述
(3)当我们遍历到 s 的时候,由于存在 s 这个节点,我们就让它的fail指针指向他父亲节点 a 的 fail 指针指向的那个节点(根)的具有相同字母的子节点(第一层的 s),也就是这样:
在这里插入图片描述
(4)按照相同规律构建第二层后,到了第三层的 h 点,还是按照上面的规则,我们找到 h 的父亲节点 s 的 fail 指针指向的那个位置(第一层的 s),然后指向它所指向的相同字母 根 -> s -> h 的这个链的 h 节点,如下图:
在这里插入图片描述
(5)按照如上的规律,完全构造好后的树如下所示:
在这里插入图片描述
3、然后匹配就很简单了,这里以 ashw 为例:
我们先用 ash 匹配,到 h 了发现,ash 是一个完整的模式串,则ans++,然后找下一个 w,可是 ash 后面没字母了呀,我们就跳到 h 的 fail 指针指向的那个 h 继续找,还是没有?再跳,结果当前的 h 指向的是根节点,又从根节点找,然而还是没有找到 w,匹配结束。流程图如下:
在这里插入图片描述

AC自动机模板题与模板

在这里选取洛谷上面的一个AC自动机的模板题,题目链接

题目内容

给定 n 个模式串 si 和一个文本串 t,求有多少个不同的模式串在文本串里出现过。
两个模式串不同当且仅当他们编号不同。

输入格式
第一行是一个整数,表示模式串的个数 n。
第 2 到第 (n + 1) 行,每行一个字符串,第 (i + 1) 行的字符串表示编号为 i 的模式串 si。
最后一行是一个字符串,表示文本串 t。

输出格式
输出一行一个整数表示答案。

输入输出样例
输入 #1
3
a
aa
aa
aaa
输出 #1
3
输入 #2
4
a
ab
ac
abc
abcd
输出 #2
3
输入 #3
2
a
aa
aa
输出 #3
2

代码详解

1、输入输出(主函数)
小编将AC自动机的函数都封装在类型为Aho_Corasick_Automaton,名为AC打的结构体中了,因此主函数只需要对输入、输出以及调用结构体里面的功能函数即可。代码如下:

int main()
{
	freopen("in.in", "r", stdin);
	freopen("out.out", "w", stdout);
	int i, j;
	scanf("%d", &n);
	//AC.initial();
	for (i = 1; i <= n; i++)
	{
		scanf("%s", p);
		AC.insert(p);        //插入模板单词
	}
	AC.build();              //建立fail指针
	scanf("%s", p);
	int ans = AC.solve(p);   //开始匹配
	cout << ans;             //输出结果
	return 0;
}

2、插入模板单词
这一部分按照如上图解中的搜索树的结构一个字母一个字母的依次建立节点即可,代码如下:

void insert(char *s)   //添加模板单词
	{
		int i;
		int len = strlen(s);
		int now = 0;       //当前节点
		for (i = 0; i < len; i++)
		{
			int v = s[i] - 'a';
			if (trie[now][v] == 0) 
			{
				cnt++;
				trie[now][v] = cnt;
			}
			now = trie[now][v];
		}
		mark[now]++;
	}

3、建立fail指针
这一部分按照如上图解中的情况,用bfs的思想进行fail数组的建立即可,代码如下:

void build()
	{
		int i;
		for (i = 0; i < 26; i++)
		{
			if (trie[0][i] != 0)
			{
				fail[trie[0][i]] = 0;
				q.push(trie[0][i]);
			}
		}
		while (!q.empty())
		{
			int u = q.front();
			q.pop();
			for (i = 0; i < 26; i++)
			{
				if (trie[u][i] != 0)
				{
					fail[trie[u][i]] = trie[fail[u]][i];
					q.push(trie[u][i]);
				}
				else
				{
					trie[u][i] = trie[fail[u]][i];
				}
			}
		}
	}

4、模板匹配
代码如下:

int solve(char *s)
	{
		int i, j;
		int len = strlen(s);
		int now = 0;
		int ans = 0;
		for (i = 0; i < len; i++)
		{
			now = trie[now][s[i] - 'a'];
			for (j = now; j && ~mark[j]; j = fail[j])
			{
				ans = ans + mark[j];
				mark[j] = -1;
			}
		}
		return ans;
	}
完整代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
int n;
char p[1000005];
struct Aho_Corasick_Automaton
{
	int trie[500010][26];  //AC自动机需要的搜索树结构
	int fail[500010];      //fail指针
	int mark[500010];      //词典结束标志
	int cnt;               //节点序号
	queue<int>q;		   //建立fail指针时需要的队列
	void initial()         //初始化
	{
		cnt = 0;
		memset(trie, 0, sizeof(trie));
		memset(fail, 0, sizeof(fail));
		memset(mark, 0, sizeof(mark));
		while (!q.empty()) q.pop();
	}
	void insert(char *s)   //添加模板单词
	{
		int i;
		int len = strlen(s);
		int now = 0;       //当前节点
		for (i = 0; i < len; i++)
		{
			int v = s[i] - 'a';
			if (trie[now][v] == 0) 
			{
				cnt++;
				trie[now][v] = cnt;
			}
			now = trie[now][v];
		}
		mark[now]++;
	}
	void build()
	{
		int i;
		for (i = 0; i < 26; i++)
		{
			if (trie[0][i] != 0)
			{
				fail[trie[0][i]] = 0;
				q.push(trie[0][i]);
			}
		}
		while (!q.empty())
		{
			int u = q.front();
			q.pop();
			for (i = 0; i < 26; i++)
			{
				if (trie[u][i] != 0)
				{
					fail[trie[u][i]] = trie[fail[u]][i];
					q.push(trie[u][i]);
				}
				else
				{
					trie[u][i] = trie[fail[u]][i];
				}
			}
		}
	}
	int solve(char *s)
	{
		int i, j;
		int len = strlen(s);
		int now = 0;
		int ans = 0;
		for (i = 0; i < len; i++)
		{
			now = trie[now][s[i] - 'a'];
			for (j = now; j && ~mark[j]; j = fail[j])
			{
				ans = ans + mark[j];
				mark[j] = -1;
			}
		}
		return ans;
	}
}AC;
int main()
{
	int i, j;
	scanf("%d", &n);
	//AC.initial();
	for (i = 1; i <= n; i++)
	{
		scanf("%s", p);
		AC.insert(p);
	}
	AC.build();
	scanf("%s", p);
	int ans = AC.solve(p);
	cout << ans;
	return 0;
}

  • 23
    点赞
  • 129
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值