~Keywords Search~~~~AC自动机

题意(这里可以点哦):有多个关键词,在一个文中找到它们。
输入:第一行是一个整数N,表示关键词个数,下面有N个关键词,N<=1000。每个关键词只包含小写字母,长度不超过50.最后一行是文本,长度不大于1000000。
输出:在输出文本中能找到多少关键词。重复的关键词只需要统计一次。

AC自动机可以说是KMP的升级版。KMP是单模匹配算法,处理在一个文本串中查找一个模式串的问题;AC自动机是多模匹配算法,能在一个文本串中同时查找多个不同的模式串。

  • KMP算法是一种在任何情况下都能达到O(n+m)复杂度的算法,它的核心——Next[ ]数组(当出现失配后,进行下一次匹配时,用Next[ ]指出 j(模式串指针)回溯的位置)。KMP算法中的getFail()函数代码如下:
void getFail(string s){//预计算Next[],用于在失配的情况下得到j回溯的位置 
	for(int i=1;i<s.size();++i){
		int j=Next[i];
		while(j&&s[j]!=s[i])j=Next[j];
		Next[i+1]=(s[i]==s[j])?j+1:0;
	}
}

其实KMP也能解决多模匹配问题,缺点就是复杂度较高,假设有k个模式串的平均长度为m,那么对每一个模式串分别做一次KMP,总复杂度是O((n+m)k)。
AC自动机算法不需要对S(文本串)做多次KMP,而是只搜索一遍S,在搜索时匹配所有模式串。
KMP是通过查找P对应的Next[ ]数组实现快速匹配的。如果把所有的P做成一个字典树,然后在匹配的时候查找这个P对应的Next[ ]数组,不就实现了快速匹配了吗?建立字典树是O(km),建立fail指针O(km),模式匹配O(nm),乘m的原因是在统计的时候需要顺着链回溯到root节点。所以总时间复杂度是O((n+k)m),当m<<k时,(n+k)m<<(n+m)k,AC自动机的优势就非常大了。

  • 字典树的代码(用数组表示,需要一个Insert()函数)很简单:
int trie[N][26],num[N],tot;//字典树
void Insert(string s){ 
	int u=0;
	for(int i=0;i<s.size();++i){
		int n=s[i]-'a';
		if(trie[u][n]==0){
			trie[u][n]=++tot;
		}
		u=trie[u][n];
	} 
	num[u]++;//当前节点单词数+1 
}

那接下来就是AC自动机的getFail()函数,我们首先定义一个数组fail[ ](失配回溯指针),然后用bfs遍历字典树的所有节点来构造每个fail指针。重点是怎么构造的,先堆公式:某节点的fail指针=父节点的fail指针指向节点的子节点(该节点代表的字母与某节点代表的字母相同) ,如果某节点不存在那就用它的键值代替它的fail指针,听起来有点绕,但还好吧。 而公式的效果是什么呢?你想想某节点的fail指针指向的节点和它有着相同的键值,那它的父节点fail指针指向的节点不也是同样的,而第一层的节点的fail指针都指向根节点0,那我们就可以把fail指针指向的字符串(从根节点到fail指针指向的节点)看做是原字符串的一个子串还是最长的子串(此处子串都有一个特点:包含尾字符),所以我们循着fail指针的路径可以遍历所有有效判断并返回根节点,简单说就是通过遍历文本串(文本串指针不需要回溯)来一堆一堆的判断模式串是否存在。

  • 如果还不明白,你就把模式串简化成一个,你会发现AC自动机竟然模拟了KMP,是不是有所明悟。废话就不多说了,直接上代码:
//AC自动机
int fail[N];//失败时的回溯指针 
void getFail(){
	queue<int>q;
	for(int i=0;i<26;++i){
		if(trie[0][i])
			q.push(trie[0][i]);
	} 
	while(!q.empty()){//bfs
		int r=q.front();
		q.pop();
		for(int i=0;i<26;++i){
			if(trie[r][i]){
				//子节点失配回溯到父节点fail指针指向节点的下一个节点 
				fail[trie[r][i]]=trie[fail[r]][i];
				q.push(trie[r][i]);
			}
			else {
				//否则子节点指向父节点fail指针指向节点的下一个节点 
				trie[r][i]=trie[fail[r]][i];
			}
		}
	}
}
  • 最后就把题解代码写出来了,多了一个没见过的query()函数,如果上述讲解听懂了,那很自然就理解了,函数中的循环就是遍历子串的意思,仅此而已。
#include<bits/stdc++.h> 
using namespace std;
const int N=1e6+1;
int trie[N][26],num[N],tot;//字典树
void Insert(string s){ 
	int u=0;
	for(int i=0;i<s.size();++i){
		int n=s[i]-'a';
		if(trie[u][n]==0){
			trie[u][n]=++tot;
		}
		u=trie[u][n];
	} 
	num[u]++;//当前节点单词数+1 
}
//AC自动机
int fail[N];//失败时的回溯指针 
void getFail(){
	queue<int>q;
	for(int i=0;i<26;++i){
		if(trie[0][i])
			q.push(trie[0][i]);
	} 
	while(!q.empty()){//bfs
		int r=q.front();
		q.pop();
		for(int i=0;i<26;++i){
			if(trie[r][i]){
				//子节点失配回溯到父节点fail指针指向节点的下一个节点 
				fail[trie[r][i]]=trie[fail[r]][i];
				q.push(trie[r][i]);
			}
			else {
				//否则子节点指向父节点fail指针指向节点的下一个节点 
				trie[r][i]=trie[fail[r]][i];
			}
		}
	}
}
int query(string s){
	int sum=0,u=0;
	for(int i=0;i<s.size();++i){
		u=trie[u][s[i]-'a'];
		for(int j=u;j&&num[j]!=-1;j=fail[j]){
			sum+=num[j];
			num[j]=-1;	
		}
	}
	return sum;
} 
int main(){
	int n;
	string s;
	cin>>n;
	for(int i=1;i<=n;++i){
		cin>>s;
		Insert(s);
	}
	getFail();
	cin>>s;
	cout<<query(s)<<endl;
	return 0;
} 
发布了7 篇原创文章 · 获赞 5 · 访问量 517

猜你喜欢

转载自blog.csdn.net/Long_hen/article/details/104603412