【AC自動機】洛谷P3808 【模板】AC自動機(簡單版)

題目鏈接:https://www.luogu.org/problem/P3808

AC自動機是看http://blog.c0per.org/2018-10/ac/https://www.luogu.org/blog/juruohyfhaha/ac-zi-dong-ji看懂的,理解怎麼操作不難,但是最主要要理解的是爲什麼這樣子操作。

首先AC自動機是在字典樹的基礎上加了一些類似kmp的思想,但是kmp是相同前後綴,這裏的失配指針是相同後綴,kmp用next指針進行跳轉,AC自動機是用一個失配指針fail進行跳轉。

構建失配指針的操作:

 對於節點i,找它父親節點的fail指針指向的那個節點j,看j的子節點中有沒有和i節點相同的:

     如果有,i節點的failz指針就指向j的那個子節點;

     如果沒有,繼續找節點j的父親節點的失配指針指向的節點循環

然後就是爲什麼要這麼操作,構建完後的樹是這樣子的:

假如模式串是shey,以第3個節點爲例:

當我們遍歷到第3個節點時,到目前位置都匹配成功,並且節點3是第一個串的末尾,cntword[3]的值不爲0,所以ans得到增加;然後就上跳到它的fail指針5,5是3的失配節點意味着,第二個串在第5個節點之前都是第一個串到第3個節點的後綴,也就是第二個串的he是第一個串she的後綴,那麼she已知匹配,he就一定能匹配成功,但5不是第二個串的末尾,cntword[5]=0,ans沒有變化;然後繼續跳到5的失配節點7,第3個串到第7個節點也一定是匹配的,且7是第三個串的末尾,ans得到增加。

然後到了下一層,因爲第3個節點實際上是沒有y這個子結點的,但是因爲它的失配指針指向的節點5有y這個子結點,所以我們在構建失配指針時,就已經將第3個節點的y子結點指向了節點6,同理這樣子跳轉過來本身就已經保證了節點6之前都是匹配成功的。

#include<iostream>
#include<cstring>
#include<string>
#include<queue>
using namespace std;
const int maxn=1e6+10;
int cnt=0;
int trie[maxn][26];//trie[i][j]表示的當前字母爲i+'a',且父節點編號爲i的節點的編號
int cntword[maxn];
int fail[maxn];
void insert(string s)
{
	int root=0;
	for(int i=0;i<s.size();i++)
	{
		int next=s[i]-'a';
		if(!trie[root][next])
			trie[root][next]=++cnt;
		root=trie[root][next];
	}
	cntword[root]++;//當前節點的單詞數+1,而不是等於1,因爲可能重複。
	//這時的root是單詞的末尾的編號,表示的是一整個單詞,途中經歷過的點的cntword是沒有自增的;
}
void getFail()//層序遍歷
{
	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();
		q.pop();
		for(int i=0;i<26;i++)
		{
			if(trie[now][i])//通過枚舉26個字母來找到now的子節點們
			{
				//該節點i的失敗指針指向它父節點now的失敗指針fail[now]的和i節點相同的節點
				fail[trie[now][i]]=trie[fail[now]][i];
				q.push(trie[now][i]);
			}
			else
				trie[now][i]=trie[fail[now]][i];//如果不存在這個節點就上跳到失配節點的等於i的這個子節點上
		}
	}
}
int query(string s)
{
	int now=0,ans=0;
	for(int i=0;i<s.size();i++)
	{
		now=trie[now][s[i]-'a'];
		for(int j=now;j&&cntword[j]!=-1;j=fail[j])//當前節點匹配完成或失配時就上跳fail指針
		{
			ans+=cntword[j];//只有j是某個單詞末尾時纔有值
			cntword[j]=-1;//記錄過的節點要標記,防止重複
		}
	}
	return ans;
}
int main()
{
	int n;
	while(cin>>n)
	{
		cnt=0;
		memset(cntword,0,sizeof(cntword));
		memset(fail,0,sizeof(fail));
		memset(trie,0,sizeof(trie));
		string a,s;
		for(int i=1;i<=n;i++)
		{
			cin>>a;
			insert(a);
		}
		getFail();
		cin>>s;
		cout<<query(s)<<endl;
	}
	return 0;
}

 

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