AC自動機(trie圖版)

AC自動機是一個多模字符串匹配的自動機(網上說的),主要作用是在一個長串中同時進行多個字符串的匹配

基礎芝士:

trie樹(字典樹)

烤饃片kmp單模字符串匹配

如果不會的建議去網上學一下(本篇講解略過)

這裏重點講一講AC自動機

(由於本蒟蒻不會指針,所以所有算法一律不使用指針,請神犇們諒解)

例:luogu3796 AC自動機(加強版)

其實AC自動機就是在trie樹上構造KMP的next指針(在AC自動機中叫fail指針),然後進行匹配

舉個例子:

模式串:

abab

abb

bab

匹配串:

aaabbbabababbba

AC自動機第一步:建立trie樹!

建樹過程略,反正建起的樹長這樣:

建樹代碼如下,基本和trie樹代碼接近:

void buildtree(char *p)
{
	int l=strlen(p);
	int now=0;
	for(int i=0;i<l;i++)
	{
		int t=p[i]-'a'+1;
		if(tree[now].to[t]==0)
		{
			tree[now].to[t]=++cnt;
			tree[tree[now].to[t]].fa=now;
			tree[tree[now].to[t]].ca=t;
		}
		now=tree[now].to[t];
	}
	tree[now].ed++;
}

接下來我們考慮構造fail指針

fail指針的含義其實就是:如果在這一位上失配了,那麼整個串不必從頭開始,而是直接從中間的某處開始繼續在失配處匹配即可

由於這是一棵trie樹,所以我們可以考慮基於bfs進行構造

首先,如果一開始就失配,那就沒啥可說的了,直接返回最大的根節點,所以在構造trie樹時一般從1開始,0作爲虛節點爲根

代碼如下:

queue <int> M;
	for(int i=1;i<=26;i++)
	{
		if(tree[0].to[i])
		{
			M.push(tree[0].to[i]);
			tree[tree[0].to[i]].fall=0;
		}
	}

接下來,我們就可以進行bfs了

這裏也是整個AC自動機中最複雜的地方

對於每個點,我們枚舉他的每一個to指針,然後分類討論:

①:這個to節點存在

(什麼叫存在?比如上面的trie樹,根據字符集來講,每個節點都應該有兩個兒子,可是事實上大部分節點都只有一個兒子,那麼有的這個兒子就叫存在,沒有就叫不存在)

那麼,這個to的fail指針應該指向他父節點的fail指針指向節點所指向的對應的to(讀二十遍)

先放代碼,再解釋,否則不好懂

if(tree[u].to[i])
			{
				tree[tree[u].to[i]].fall=tree[tree[u].fall].to[i];
				M.push(tree[u].to[i]);
			}

解釋一下,就像這樣:

其中藍色的線爲fail指針

發現什麼了嗎?

一個點fail指針所指向的點所在字符串的前綴一定是這個點所在字符串的子串!

舉個例子:

如圖所示,右邊紅色框裏的字符串的前綴是左邊紅色字符串的一個子串,因爲左邊的b指向了右邊的b

(當然,這個前綴理論僅適用於fail指針指向的節點之前的前綴,而之後的是無法保證的)

但是我們會發現一個bug:看到第二個串的最後一個b了嗎?他的fail指針應該指向他父節點的fail指針指向節點的對應節點,可是..沒有這個節點啊...

直接指回根節點?

這不太好

因爲明明有能匹配上的啊

所以我們要利用trie圖思想了。

trie圖與AC自動機少數的不同就是trie圖會補全所有的子節點,補全方法是指向這個點父節點的fail指針指向節點的對應節點

else
			{
				tree[u].to[i]=tree[tree[u].fall].to[i];
			}

所以這也就是上面所述的分類討論的第二種情況:如果這個節點不存在,那麼要把這個節點的指針建起來

這樣就可以指了

最後構造好的fail指針長這樣:

其中綠色的是特殊構造出來的fail指針

fail指針都完事了,接下來就好辦了。

我們將模式串在這個AC自動機上跑

查詢操作:

int query(char *p)
{
	int l=strlen(p);
	int ans=0;
	tot=0;
	int now=0;
	for(int i=0;i<l;i++)
	{
		int t=p[i]-'a'+1;
		now=tree[now].to[t];
		int temp=now;
		while(temp)
		{
			if(tree[temp].ed>ans)
			{
				memset(ret,0,sizeof(ret));
				tot=0;
				ret[++tot]=temp;
				ans=tree[temp].ed;
			}else if(tree[temp].ed==ans)
			{
				ret[++tot]=temp;
			}
			if(tree[temp].ed)
			{
				tree[temp].ed++;
			}
			temp=tree[temp].fall;
		}
	}
	return ans;
}

稍微解釋一下,就是順着trie樹跑匹配串,根據上文所述fail指針的性質,每次向前找一個前綴使得這個前綴是這個匹配串的子串,於是我們總是能找到整個串是這個字符串的子串

還有一步操作很重要,即上面的最後一個if,這一步的操作目的在於累計某個串被匹配上的次數

這樣就完事了

貼代碼:

#include <cstdio>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <queue>
#include <stack>
using namespace std;
struct Trie
{
	int to[27];
	int fa;
	int fall;
	int ca;
	int ed;
}tree[1000005];
int ret[155];
char s[1000005];
int cnt=0;
int tot=0;
void buildtree(char *p)
{
	int l=strlen(p);
	int now=0;
	for(int i=0;i<l;i++)
	{
		int t=p[i]-'a'+1;
		if(tree[now].to[t]==0)
		{
			tree[now].to[t]=++cnt;
			tree[tree[now].to[t]].fa=now;
			tree[tree[now].to[t]].ca=t;
		}
		now=tree[now].to[t];
	}
	tree[now].ed++;
}
void getfail()
{
	queue <int> M;
	for(int i=1;i<=26;i++)
	{
		if(tree[0].to[i])
		{
			M.push(tree[0].to[i]);
			tree[tree[0].to[i]].fall=0;
		}
	}
	while(!M.empty())
	{
		int u=M.front();
		M.pop();
		for(int i=1;i<=26;i++)
		{
			if(tree[u].to[i])
			{
				tree[tree[u].to[i]].fall=tree[tree[u].fall].to[i];
				M.push(tree[u].to[i]);
			}else
			{
				tree[u].to[i]=tree[tree[u].fall].to[i];
			}
		}
	}
}
int query(char *p)
{
	int l=strlen(p);
	int ans=0;
	tot=0;
	int now=0;
	for(int i=0;i<l;i++)
	{
		int t=p[i]-'a'+1;
		now=tree[now].to[t];
		int temp=now;
		while(temp)
		{
			if(tree[temp].ed>ans)
			{
				memset(ret,0,sizeof(ret));
				tot=0;
				ret[++tot]=temp;
				ans=tree[temp].ed;
			}else if(tree[temp].ed==ans)
			{
				ret[++tot]=temp;
			}
			if(tree[temp].ed)
			{
				tree[temp].ed++;
			}
			temp=tree[temp].fall;
		}
	}
	return ans;
}
bool cmp(int a,int b)
{
	return a<b;
}
void init()
{
	memset(ret,0,sizeof(ret));
	memset(tree,0,sizeof(tree));
	cnt=0;
	tot=0;
}
void print(int rt)
{
	if(!rt)
	{
		return;
	}
	print(tree[rt].fa);
	printf("%c",tree[rt].ca-1+'a');
}
int main()
{
	int n;
	while(1)
	{
		scanf("%d",&n);
		if(n==0)
		{
			return 0;
		}
		init();
		for(int i=1;i<=n;i++)
		{
			scanf("%s",s);
			buildtree(s);
		}
		getfail();
		scanf("%s",s);
		printf("%d\n",query(s));
		sort(ret+1,ret+tot+1,cmp);
		for(int i=1;i<=tot;i++)
		{
			print(ret[i]);
			printf("\n");
		}
	}
	return 0;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章