AC自动机(从入门到放弃)

一.前言

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

其前置知识是:KMP字典树

二.前置知识字典树

1.基本概念

字典树是一棵包含所有模板串的26叉树。

树的”边权“是字符,从根节点出发到任意点的”距离“,即为某一字符串的前缀。

2.建立字典树

指针版

用next[26]指针来表示每一叉所指向的26子叉
用fail存储每个节点的失配情况(不懂没关系)
用sum表示这个节点是不是一个单词的结尾,以及相应的个数

struct node {
    
    
	node *next[26];//每一个字符子叉
	node *fail;//实配指针
	int cntword;//表示是否是单词结尾,以及相应的个数
};
queue<node*>q;
char t[1000005];//模板串
char s[1000005];//文本串
node *root;//字典树的根

//建立字典树
void insertWords(char *t) {
    
    
	node *p=root;
	for(int i=0; t[i]; i++) {
    
    
		int x=t[i]-'a';
		if(p->next[x]==NULL) {
    
    
			node *newnode=(struct node *)malloc(sizeof(struct node));//新建一个点 
			for(int j=0; j<26; j++)newnode->next[j]=0;
			newnode->cntword=0;
			newnode->fail=0;
			p->next[x]=newnode;
		}
		p=p->next[x];
	}
	p->cntword++;//当前节点单词数+1 
}

数组版

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 
}

三.前置知识KMP(运用到其思想)

一个大佬的超详细理解,建议直接看大佬的

1.基本概念

KMP是一种匹配单一模板串t和文本串s的算法
即判断:模板串是否为文本串的子串

2.算法实现

暴力:枚举模板串在文本串的开始位置即可

int force(string s,string t){
    
    //从目标串s中匹配模板串t 
	int i=0,j=0;
	while(i<s.length()&&j<t.length()){
    
    
		if(s[i]==t[j])i++,j++;
		else i=i-j+1,j=0;//每次都要完整的重新比较,即s串向右移一位,t串从0重新开始 
	}
	if(j==t.length())return i-j;
	else return -1;
} 

如何优化?KMP
每次都重新匹配时间复杂度太高了,我们能否在每次匹配失败的时候,只让模板串的t指针回到一个恰当的位置,使得该位置前的所有字符串已匹配成功。即维护一个next[]表示每次失配时模板串t需要回到的位置。

int KMP(string s,string t){
    
    //从目标串s中匹配模板串t 
    KMP_init(t);
	int i=0,j=0;
	while(i<s.length()&&j<t.length()){
    
    
		if(s[i]==t[j]||j==-1)i++,j++;
		else j=next[j];//[0,next[j]-1]匹配完成,t串直接回退到next[j]位置即可 
	} 
	if(j==t.length())return i-j;
	else return -1;
}

如何预处理这个模板串t的next数组???
当模板记吧

我也不会,等我会了再讲

void KMP_init(string t){
    
    
	int j=0,k=-1;
	next[0]=-1;
	while(j<t.length()-1){
    
    
		if(t[j]==t[k]||k==-1){
    
    //j指向模板串t当前正常维护的元素下标,k
			next[++j]=++k;
		}
		else k=next[k];
	} 
} 

四.AC自动机

1.基本理解

KMP是个一维数组,next[j]为每个模板串t的第j个字符的失配指针
那么AC自动机,就是在字典树上,求每个位置的失配指针

2.算法实现

大佬的顶级解释

需要实现的操作有:
1.建立字典树(上面已写)
2.建立失配指针(俺也不会,当模板敲 )
3.ac_automaton(俺还是不会,当模板敲 )

代码奉上(指针版)

#include<bits/stdc++.h>
using namespace std;

const int N=1e6+10;

struct node {
    
    
	node *next[26];//每一个字符子叉
	node *fail;//实配指针
	int cntword;//表示是否是单词结尾,以及相应的个数
};
queue<node*>q;
char t[1000005];//模板串
char s[1000005];//文本串
node *root;//字典树的根

//建立字典树
void insertWords(char *t) {
    
    
	node *p=root;
	for(int i=0; t[i]; i++) {
    
    
		int x=t[i]-'a';
		if(p->next[x]==NULL) {
    
    
			node *newnode=(struct node *)malloc(sizeof(struct node));//新建一个点 
			for(int j=0; j<26; j++)newnode->next[j]=0;
			newnode->cntword=0;
			newnode->fail=0;
			p->next[x]=newnode;
		}
		p=p->next[x];
	}
	p->cntword++;//当前节点单词数+1 
}

//建立字典树每个节点的失配指针fail
void build_fail() {
    
    
	q.push(root);
	node *p;
	node *temp;
	while(!q.empty()) {
    
    
		temp=q.front();
		for(int i=0; i<26; i++) {
    
    
			if(temp->next[i]) {
    
    
				if(temp==root)temp->next[i]->fail=root;
				else {
    
    
					p=temp->fail;
					while(p) {
    
    
						if(p->next[i]) {
    
    
							temp->next[i]->fail=p->next[i];
							break;
						}
						p=p->fail;
					}
					if(p==NULL)temp->next[i]->fail=root;
				}
				q.push(temp->next[i]);
			}
		}
		q.pop();
	}
}

void ac_automaton(char *s) {
    
    
	node *p=root;
	int ans=0;
	for(int i=0;s[i]!='\0'; i++) {
    
    
		int x=s[i]-'a';
		while(p->next[x]==NULL&&p!=root)p=p->fail;
		p=p->next[x];
		if(p==NULL)p=root;
		node *temp=p;
		while(temp!=root) {
    
    
			if(temp->cntword>=0) {
    
    
				ans+=temp->cntword;
				temp->cntword=-1;//将遍历过后的节点标记,防止重复计算 
			} else break;
			temp=temp->fail;
		}
	}
	return ans;
}

int main() {
    
    
	int T,n;
	cin>>T;
	while(T--) {
    
    
		//初始化字典树树根
		root=(struct node*)malloc(sizeof(struct node));
		for(int i=0; i<26; i++)root->next[i]=0;
		root->fail=0;
		root->cntword=0;

		//建立字典树trie
		cin>>n;
		for(int i=1; i<=n; i++) {
    
    
			cin>>t;
			insertWords(t);
		}
		
		//建立字典树的失配指针
		build_fail();

		//AC自动机自动匹配
		cin>>s;
		cnt=0;
		cout<<ac_automaton(s)<<endl;
	}
	return 0;
}

代码奉上(数组版)

#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 t;//模板串 
queue<int>q;//bfs求fail的时候用到 

void insertWords(string 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 
}
void build_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_automaton(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>>t;
			insertWords(t);
		}
		build_fail();
		cin>>s;
		cout<<ac_automaton(s)<<endl;
	}
	return 0;
}

3.开始刷题

模板题1

例题2

题目大意:求出现在文本串中最多次的模板串
题解
不存在重复字符串,且每个字符串可以重复计数
1.那么到每个叶子节点的字符串cntword始终为1,且到trie的每一个前缀,要么为1,要么为0。
2.因为每个模板串都不止被文本串走1次,故在ac_automaton的时候,不能标记cntword为-1来防止下一次经过
3.为了记录每个模板串的出现次数和字符串的本身,需要一个结构体存储,并排序。在ac_automaton的时候记录每一个出现的模板串的出现次数和模板编号。
4.为了实现2的模板编号查找,模板串需离线,且我们需要有一个mark数组存储每一个insertwords的时候的叶节点所对应模板编号。

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

int trie[N][26];
int cntword[N];
int fail[N];
int cnt;
int mark[N];
int vis[N];
string s;
string t[160];
queue<int>q;
struct ppp {
    
    
	int pos,num;
} ans[160];
int top;

int cmp(ppp x,ppp y) {
    
    
	if(x.num==y.num)return x.pos<y.pos;
	else return x.num>y.num;
}

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[now]=pos;
}
void build_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++) {
    
    
			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 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]) {
    
    
			if(vis[mark[j]]==0) {
    
    
				ans[++top].num=cntword[j];
				ans[top].pos=mark[j];
				vis[mark[j]]=top;
			}
			else {
    
    
				ans[vis[mark[j]]].num+=cntword[j];
			}

		}
	}
	sort(ans+1,ans+top+1,cmp);
	int maxx=ans[1].num;
	cout<<maxx<<endl;
	for(int i=1; i<=top; i++)if(ans[i].num==maxx)cout<<t[ans[i].pos]<<endl;
}
int main() {
    
    
	int n;
	while(1) {
    
    
		cin>>n;
		if(n==0)return 0;
		memset(cntword,0,sizeof(cntword));
		memset(fail,0,sizeof(fail));
		memset(trie,0,sizeof(trie));
		memset(ans,0,sizeof(ans));
		memset(vis,0,sizeof(vis));
		top=0;

		for(int i=1; i<=n; i++) {
    
    
			cin>>t[i];
			insertWords(t[i],i);
		}
		build_fail();
		cin>>s;
		ac_automaton(s);
	}
	return 0;
}

例题3

题目大意:求每个模板串在文本串中出现的次数
题解
1.题目不保证模板串不一致,需进行一些操作,防止重复insert,这里采用的是map。
2.因为要求每个模板串出现的次数,所以要记下每个模板串在trie树上的终止节点
3.暴力跑fail边很好想但是T,因为会被类似aaaa的数据卡掉

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; 
}

4.正解:建出 fail 树,记录自动机上的每个状态被匹配了几次,最后求出每个模式串在 Trie 上的终止节点在 fail 树上的子树总匹配次数就可以了。

#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;
queue<int>q;
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() {
    
    
	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;
}

猜你喜欢

转载自blog.csdn.net/weixin_43602607/article/details/110875126