【算法详解 && 字符串】 AC自动机

什么是AC自动机??

  • AC自动机可以说是KMP的一个强化版,KMP是用来求两个字符串之间的匹配,而AC自动机则是用来求一堆字符串与一个字符串之间的匹配,思路与KMP相似,但却需要用到一些数据结构来维护字符(字典序)‘

在AC自动机中,我们需要用到trie树(字典树)以及KMP的思路,也就是说,我们需要用trie树来维护KMP,或者说在树上做KMP。

在AC自动机中,有一个很重要的一个数组,它就是fail数组fail数组对于实现AC自动机有着莫大的意义。

fail数组表示什么???
  • 我们需要将思路转化到trie树上,fail[i] 表示后缀为 s [ x ] [ i . . n ] s[x][i..n] s[x][i..n](注:这里的n不是指有多少个字符串,而是指trie中∈i的分枝的最深的深度) = = s [ y ] [ k . . w ] ==s[y][k..w] ==s[y][k..w]的最小k的值(其中 x , y ∈ [ 0..25 ] x,y∈[0..25] x,y[0..25],因为trie树可以认为是26叉树,指的是分在第几叉),指的是后缀的最长公共前缀的那个节点的编号,使用这个数组能够很好的在树上进行KMP操作

下面有一张很好的图来解释 f a i l fail fail数组的含义:
在这里插入图片描述
其中:
f a i l [ 4 ] fail[4] fail[4]所拥有的最长公共前缀就是8号节点
f a i l [ 3 ] fail[3] fail[3]所对应的是 c d cd cd,最长公共前缀是7号,也是 c d cd cd
上图就是这个意思,每个点都连向了它的fail,大家感性理解便可

如何转移fail?
  • 分两种情况,如果有下一个节点,那么就令 f a i l [ c [ n o w ] [ i ] ] = c [ f a i l [ n o w ] ] [ i ] fail[c[now][i]] = c[fail[now]][i] fail[c[now][i]]=c[fail[now]][i];否则就让 c [ n o w ] [ i ] = c [ f a i l [ n o w ] ] [ i ] c[now][i] = c[fail[now]][i] c[now][i]=c[fail[now]][i]
  • 比较好理解,大家自行理解

这里AC自动机是需要用用trie树实现的,我们这里来说一说如何建trie树。

如何建树

  • 读入一个字符串,我们扫每一个字符,并且判断当前层是否出现过当前的字符,如果没出现,就建立一个点,继续往下扫,否则就继续往下扫。
Code
    void ins(string s){
	    int len = s.size();
	    int now = 0;
	    for (int i=0;i<len;i++){
		    if (!c[now][s[i]-'a']) c[now][s[i]-'a'] = ++cnt;//如果没出现,就加一个节点
		    now = c[now][s[i] - 'a'];//往下扫
		}
		val[now] ++;
	}

求fail数组

我们已经知道了求fail的大概思想,这是想如何实现。
我们尝试用队列维护,把每个节点存入队列从上往下遍历,做一遍更新即可

Code
	void build(){
		queue < int > q;
	    for (int i=0;i<26;i++)
	      if (c[0][i]) q.push(c[0][i]),fail[c[0][i]] = 0;/先将第0层的给搞出来
	    while (!q.empty()){
		    int now = q.front();q.pop();
		    for (int i=0;i<26;i++)
		      if (c[now][i]) fail[c[now][i]] = c[fail[now]][i],q.push(c[now][i]);//如果有的话,转移,入队
		      else c[now][i] = c[fail[now]][i];//否则就令下一个节点到它下一个后缀的节点中
	    }
	}

求答案

  • 接下来就是求答案了
  • 我们遍历一遍模式串,找出它每一位对应的在树上的节点,不断累加、遍历即可。
	int ask(string s){
	    int now = 0 , ans = 0;
	    int len = s.size();
	    for (int i=0;i<len;i++){
		    int ch = s[i] - 'a';
		    now = c[now][ch];
		    int t = now;
		    for (;t;t = fail[t]) ans += val[t];//利用fail数组不断跳跃求和
		}
		return ans;
	}

这样便基本能够实现AC自动机了。


接下来给大家上几道AC自动机模板题。

一、AC自动机模板题1

给定n个模式串和1个文本串,求有多少个模式串在文本串里出现过。

题目传送门

Solution

这题基本上就是AC自动机模板,只不过需要注意的是这里求的是有多少个模式串出现过,而不是出现了几次,所以我们在遍历的时候打一个标记即可。

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

struct Tr{
    int c[1010010][27];
    int cnt = 0;
    int val[10101000];
    int fail[10100100];
    void ins(string s){
	    int len = s.size();
	    int now = 0;
	    for (int i=0;i<len;i++){
		    if (!c[now][s[i]-'a']) c[now][s[i]-'a'] = ++cnt;
		    now = c[now][s[i] - 'a'];
		}
		val[now] ++;
	}
	void build(){
		queue < int > q;
	    for (int i=0;i<26;i++)
	      if (c[0][i]) q.push(c[0][i]),fail[c[0][i]] = 0;
	    while (!q.empty()){
		    int now = q.front();q.pop();
		    for (int i=0;i<26;i++)
		      if (c[now][i]) fail[c[now][i]] = c[fail[now]][i],q.push(c[now][i]);
		      else c[now][i] = c[fail[now]][i];
		}
	}
	int ask(string s){
	    int now = 0 , ans = 0;
	    int len = s.size();
	    for (int i=0;i<len;i++){
		    int ch = s[i] - 'a';
		    now = c[now][ch];
		    int t = now;
		    for (;t && val[t]!=-1;t = fail[t]) ans += val[t] , val[t] = -1;
		}
		return ans;
	}
};

Tr tr;

int main(){
	freopen("1.in","r",stdin);
	freopen("1.out","w",stdout);
	string S;
    scanf("%d",&n);
    for (int i=1;i<=n;i++)
      cin>>S,tr.ins(S);
    tr.build();
    cin>>S;
    printf("%d",tr.ask(S));
    return 0;
}

二、AC自动机模板题2

有N个由小写字母组成的模式串以及一个文本串T。每个模式串可能会在文本串中出现多次。你需要找出哪些模式串在文本串T中出现的次数最多。

Solution

这道也算是一个裸题了。
在建树的时候打个标记,看当前结尾的字符串是第几个串,统计的额时候累加即可

Code

#include<bits/stdc++.h>
using namespace std;
int n;
string ss[10100];
struct Tr{
    int cnt;
    int fail[152032];
    int c[100100][26];
    int num[101000];
    int numn[101000];
    int maxx = 0;
    int len=0;
    void mem(){
	    cnt = 0,len=0,maxx=0;
	    memset(fail,0,sizeof fail);
	    memset(num,0,sizeof num);
	    memset(numn,0,sizeof numn);
	    memset(c,0,sizeof c);
	}
    void ins(string s,int Num){
	    int len = s.size();
		int now = 0;
		for (int i=0;i<s.size();i++){
		    int ch = s[i]-'a';
		    if (!c[now][ch]) c[now][ch] = ++cnt;
		    now = c[now][ch];
		}
//		val[now] ++;
		num[now] = Num;
	}
	void build(){
	    queue < int > q;
	    for (int i=0;i<26;i++)
	      if (c[0][i]) fail[c[0][i]] = 0,q.push(c[0][i]);
	    while (!q.empty()){
		    int now = q.front();q.pop();
		    for (int i=0;i<26;i++)
		      if (c[now][i]) fail[c[now][i]] = c[fail[now]][i],q.push(c[now][i]);
		      else c[now][i] = c[fail[now]][i];
		}
	}
	void ask(string s){
	    int len = s.size();
	    int now = 0;
	    for (int i=0;i<s.size();i++){
		    int ch = s[i] - 'a';
		    now = c[now][ch];
		    int t=now;
		    for (;t;t = fail[t]) numn[num[t]] += 1;
		}
		for (int i=1;i<=n;i++)
		  if (numn[i] > maxx) maxx = numn[i];
		printf("%d\n",maxx);
		for (int i=1;i<=n;i++)
		  if (numn[i] == maxx) cout<<ss[i]<<endl;
	}
};

Tr tr;

void work(){
    string S;
    tr.mem();
    for (int i=1;i<=n;i++)
      cin>>ss[i],tr.ins(ss[i],i);
    tr.build();
    cin>>S;
    tr.ask(S);
}

int main(){
//	freopen("1.in","r",stdin);
//	freopen("1.out","w",stdout);
	while (cin>>n && n) work();
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值