AC自动机

一.个人理解

  • AC自动机是一种字符串匹配的算法,用来将多个模板串t与文本串s匹配。例如:求解文本串中包含了多少个模板串

  • 其前置知识是:KMP字典树,貌似不学KMP也没事,只是AC自动机运用到了和 KMP 类似的思想,失配指针。

  • K M P KMP KMP n x t nxt nxt 指针与 a c ac ac 自动机的 f a i l fail fail 指针类似,都是当匹配失败时,回到最长的一个前缀和后缀相同的最长前缀。

前置知识字典树

1.基本概念

  • 字典树是一棵包含所有模板串的26叉树。
  • 树的”边权“是字符,从根节点出发到任意点的”距离“,即为某一字符串的前缀。

2.建立字典树

int trie[N][26];//字典树trie 
int cntword[N];//计算该单词出现次数 
int fail[N];//失配指针 
int cnt;//动态开点 

void insertWords(char *t){
	int now=0;
	for(int i=0;t[i]!='\0';i++){
		int next=t[i]-'a';
		if(!trie[now][next])trie[now][next]=++cnt;
		now=trie[now][next];
	}
	cntword[now]++;//当前单词数+1 
}

二.算法学习

大佬直通车

0.黑箱

  • 如果你学完还不明白,你需要记住以下几点
  • 1.若 y y y f a i l fail fail 指针连向 x x x,说明 x x x 出现在 y y y 对应的字符串的后缀中。
  • 2. b u i l d _ f a i l build\_fail build_fail 后, t r i e trie trie 树会发生变化(原来的边都不会发生变化,只是会多一些边),想要原来的 t r i e trie trie 树可以考虑备份一棵 t r i e trie trie 树。
  • 3. b u i l d _ f a i l build\_fail build_fail 前, t r i e trie trie 是一棵树, b u i l d _ f a i l build\_fail build_fail 后, t r i e trie trie 是一个永通路

1.基本理解

  • KMP是个一维数组, n x t [ j ] nxt[j] nxt[j] 为模式串 p p p 的第 j j j 个字符的失配指针
  • 那么 A C AC AC 自动机,就是在字典树上,求每个位置的失配指针。

2.失配指针是啥,有啥作用???

  • w o r d [ i ] word[i] word[i] 表示从根到第 i i i 个结点路径形成的单词
  • 如果 t = f a i l [ j ] t=fail[j] t=fail[j] ,则 w o r d [ t ] word[t] word[t] w o r d [ j ] word[j] word[j] 的一个在字典树上存在的最长后缀。很显然 t < j t<j t<j
  • 若已知一串字符串 s = h e r s s=hers s=hers,则可以通过不断的跳 f a i l fail fail 指针,来计算 h e r s hers hers 的所有后缀串 e r s , r s , s ers,rs,s ers,rs,s出现的次数。
  • e r , r er,r er,r呢?在计算 h e r her her 的所有后缀串的时候就算好了!
  • 所以 a c ac ac 自动机的过程就是对于每个在字典树上存在的文本串,都求一遍这个文本串的所有后缀串出现的次数。不断加长文本串,如果文本串没有在字典树上出现,就重来,即文本串清 0 0 0
  • 例如:求 h e r s h i t hershit hershit 在字典树上出现,但 s s s 没有出现。那么ac自动机的过程就是:求 h , h e , h e r , h , h i , h i t h,he,her,h,hi,hit h,he,her,h,hi,hit 出现的次数。

AC自动机的过程

int ac(string s){
	int now=0,ans=0;
	for(int i=0;s[i]!='\0';i++){//遍历文本串 
	    now=trie[now][s[i]-'a'];//从s[i]开始寻找 
	    
	    //一直向下寻找,直到匹配失败(失败指针指向根或者当前节点已找过)
	    for(int j=now;j!=0&&cntword[j]!=-1;j=fail[j]){
	    	ans+=cntword[j];
	    	cntword[j]=-1;//防止重复计算 
		}
	}
	return ans;
} 

3.怎么求fail数组呢???

  • 如果求fail数组,即如何求字典树上的每个串的最长后缀子串的位置。
  • 我们可以使用bfs层序遍历,很显然,对于一个串的最长后缀字串,其串的深度一定是大于它的最长后缀字串的深度的,bfs的过程中我们会先算深度小的,再算深度大的。
  • 考虑两种情况:
  • 如果存在这个节点,就让这个节点的失配指针指向其父节点在i失配的指针所指向的点 。
  • 如果不存在这个节点,就让这个节点与其父节点在i失配的指针所指向的点,连一条边
void build_fail(){
	queue<int>q;
    for(int i=0;i<26;i++){
    	if(trie[0][i]){
    		fail[trie[0][i]]=0;//第二层的点失配指针指向根节点,跑fail的时候跑到0就婷婷了
    		q.push(trie[0][i]);//第二层的点扔进队列中 
		}
	}
	
	while(!q.empty()){
		int now=q.front();
		for(int i=0;i<26;i++){
		    //如果存在这个节点,就让这个节点的失配指针指向其父节点在i失配的指针所指向的点 
			if(trie[now][i]){
				fail[trie[now][i]]=trie[fail[now]][i];
				q.push(trie[now][i]);
			}
			//如果不存在这个节点,就让这个节点与其父节点在i失配的指针所指向的点,连一条边 
			else {
				trie[now][i]=trie[fail[now]][i];
			}
		}
		q.pop();
	}

模板如下

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;

int trie[N][26];//字典树trie 
int cntword[N];//计算该单词出现次数 
int fail[N];//失配指针 
int cnt;//动态开点 
string s;//文本串 
string p;//模板串 

void insertWords(string p){
	int now=0;
	for(int i=0;p[i]!='\0';i++){
		int next=p[i]-'a';
		if(!trie[now][next])trie[now][next]=++cnt;
		now=trie[now][next];
	}
	cntword[now]++;//当前单词数+1 
}
void build_fail(){
	queue<int>q;//bfs求fail的时候用到 
    for(int i=0;i<26;i++){
    	if(trie[0][i]){
    		fail[trie[0][i]]=0;//第二层的点失配指针指向根节点 
    		q.push(trie[0][i]);//第二层的点扔进队列中 
		}
	}
	
	while(!q.empty()){
		int now=q.front();
		for(int i=0;i<26;i++){
		    //如果存在这个节点,就让这个节点的失配指针指向其父节点在i失配的指针所指向的点 
			if(trie[now][i]){
				fail[trie[now][i]]=trie[fail[now]][i];
				q.push(trie[now][i]);
			}
			//如果不存在这个节点,就让这个节点与其父节点在i失配的指针所指向的点,连一条边 
			else {
				trie[now][i]=trie[fail[now]][i];
			}
		}
		q.pop();
	}
}
int ac(string s){
	int now=0,ans=0;
	for(int i=0;s[i]!='\0';i++){//遍历文本串 
	    now=trie[now][s[i]-'a'];//从s[i]开始寻找 
	    
	    //一直向下寻找,直到匹配失败(失败指针指向根或者当前节点已找过)
	    for(int j=now;j!=0&&cntword[j]!=-1;j=fail[j]){
	    	ans+=cntword[j];
	    	cntword[j]=-1;//防止重复计算 
		}
	}
	return ans;
} 
int main(){
	int T,n;
	cin>>T;
	while(T--){
		memset(cntword,0,sizeof(cntword));
		memset(fail,0,sizeof(fail));
		memset(trie,0,sizeof(trie));
		cin>>n;
		for(int i=1;i<=n;i++){
			cin>>p;
			insertWords(p);
		}
		build_fail();
		cin>>s;
		cout<<ac(s)<<endl;
	}
	return 0;
}

三.算法训练

例题1:模板题

模板题链接

  • 题目描述: n n n 个模式串 p p p ,求有多少个模式串在文本 s s s 中出现过。
  • 问题分析: 直接跑 a c ac ac 自动机,时间复杂度 O ( ∣ s ∣ + ∑ ∣ p ∣ ) O(|s|+\sum |p|) O(s+p)

例题2:fail 树的基础应用

例题链接1
例题链接2

  • 题目描述: n n n 个模式串 p p p(不保证不同),求每个模式串在文本串 s s s 中分别出现了多少次。
  • 问题分析:
  • 因为要求每个模板串出现的次数,所以要映射一下模板串在 t r i e trie trie 树上的终止节点
  • 现在不能用 c n t w o r d [ j ] = − 1 cntword[j]=-1 cntword[j]=1 来避免重复计数,因为要求每个模式串 p p p 出现的次数
  • 仍然暴力跳 f a i l fail fail 边???
  • 若出现 a a a a a a aaaaaa aaaaaa 的数据,会被卡掉
void ac_automaton(string s) {
	int now=0;
	for(int i=0; s[i]!='\0'; i++) {
		now=trie[now][s[i]-'a'];
		for(int j=now; j!=0; j=fail[j]) {
			num[j]+=cntword[j];
		}
	}
	for(int i=1; i<=n; i++)cout<<num[mark[i]]<<endl; 
}
  • 方法: 建立 f a i l fail fail 树,将 s s s 在自动机上的前缀对应的结点打上标记 s i z e [ i ] = 1 size[i]=1 size[i]=1,然后求 f a i l fail fail 树的 s i z e [ u ] size[u] size[u] 和 。这样每个模式串匹配的次数就是 s i z e [ u ] size[u] size[u] u u u 表示该模式串对应的结点。
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;

int trie[N][26];
int cntword[N];
int fail[N];
int cnt,n;
int size[N];
int mark[N];
int fanmark[N];
string s;
string t;
map<string,int>m;

struct ppp{
	int u,v,next;
}e[N*2];
int vex[N],k;

void add(int u,int v){
	k++;
	e[k].u=u;
    e[k].v=v;
    e[k].next=vex[u];
    vex[u]=k;
} 

void insertWords(string t,int pos) {
	int now=0;
	for(int i=0; t[i]!='\0'; i++) {
		int next=t[i]-'a';
		if(!trie[now][next])trie[now][next]=++cnt;
		now=trie[now][next];
	}
	cntword[now]++;
	mark[pos]=now;
	fanmark[now]=pos;
}
void build_fail() {
	queue<int>q;
	for(int i=0; i<26; i++) {
		if(trie[0][i]) {
			fail[trie[0][i]]=0;
			q.push(trie[0][i]);
		}
	}
	while(!q.empty()) {
		int now=q.front();
		for(int i=0; i<26; i++) {
			if(trie[now][i]) {
				fail[trie[now][i]]=trie[fail[now]][i];
				q.push(trie[now][i]);
			} else {
				trie[now][i]=trie[fail[now]][i];
			}
		}
		q.pop();
	}
}

void dfs(int u){
	for(int i=vex[u];i;i=e[i].next){
		int v=e[i].v;
		dfs(v);
		size[u]+=size[v];
	}
}

void ac_automaton(string s) {
	int now=0;
	for(int i=0; s[i]!='\0'; i++) {
		now=trie[now][s[i]-'a'];
		size[now]++;
	}
	for(int i=1;i<=cnt;i++)add(fail[i],i);
	dfs(0);
	for(int i=1; i<=n; i++)cout<<size[mark[i]]<<endl; 
}
int main() {
	cin>>n;
	for(int i=1; i<=n; i++) {
		cin>>t;
		if(m[t])mark[i]=mark[m[t]];
		else {
			m[t]=i;
			insertWords(t,i);
		}
	}
	build_fail();
	cin>>s;
	ac_automaton(s);
	return 0;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值