回文自動機[學習筆記]


回文自動機一一處理回文串問題的有力武器

這幾天一直沉迷字符串數據結構

看了很多大佬的回文自動機學習筆記,稍微有點理解了,整理一下吧

1.概念

\(\quad\)a.大概: 同其他自動機一樣,回文自動機是個DAG,它用相當少(\(O(n)\))的空間復雜度就存儲了這個字符串的所有回文串信息。一個回文自動機包含不超過\(|S|\)個節點,每個節點都表示了這個字符串的一個不重復的回文子串,同時一個節點會有不超過字符集大小的邊連向其他節點,以及一條fail邊連向這個點的fail...這些都會在下面介紹

\(\quad\)b.森林: 和別的自動機不太一樣,回文自動機是有兩棵樹的森林:其中一棵是長度為偶數的回文串集合,另一棵是長度為奇數的回文串集合,這兩棵樹的根節點分別表示長度為0(空串)和-1(無實際含義,便於運算)的回文串;

\(\quad\)c.邊:自動機中每條有向邊都有一個字符類型的權值,起點的串左右分別加上這個字符得到的就是終點的串。舉個栗子:設一條邊權為\(c\)的邊連接的兩個點分別是\(A,B\)\(A\)表示回文串\(aba\),則\(B\)表示的回文串就是\(cabac\) 。特別的,如果\(A\)是那個長度為\(-1\)的根,\(B\)串就是這條邊的權值。。。

\(\quad\)d.點:當你插入一個字符的時候,插入的點代表的就是這個字符匹配的最長回文串,也就是說從根節點往下順着邊走,記着一個str一開始為空,一邊走一邊不停地往str左右兩邊添加新的字符,走到一個點,這個點代表的回文串就是str

\(\quad\)e.\(fail\)邊:每個點都有個fail邊,這條邊指向這個點所代表的回文串的 最長回文后綴 所在的那個點(最長回文后綴:串中滿足回文的最長的后綴,這個串自己不算)如果沒有,則指向0(就是那個根節點)。特別的,0的fail節點就是那個長度為-1的點。

2.構造:

\(\quad\)我是用的一個結構體存的,\(len,fail,son[26],siz\) 分別代表這個串的長,fail節點,連出來的每一條邊以及這個回文串的數量,如下

struct node{
	int len,fail,son[26],siz;
};
node prt[maxn]; 

我們把兩個根下標設為0和1,並根據上面介紹的給他們賦值

	prt[1].len=-1;
	prt[0].fail=prt[1].fail=1;

然后我們就可以把點一個一個加入到回文自動機中,這可以用一個函數\(extend\)來實現,具體實現方法如下:

設我們以前插入的最后一個點為\(last\),這次要插入一個點x,首先要找到一個點\(cur\)為滿足前面的字符等於新加入字符的,\(last\)的最長的回文后綴,這個過程可以不停地在\(last\)\(fail\)鏈上跑,因為\(fail\)所對應的正是串的最長回文后綴,這個可以用下面函數實現:

int getfail(int x){
	while(s[n-prt[x].len-1]!=s[n]) x=prt[x].fail;
	return x;
}

\(cur\)已經包含權值為x的出邊了,我們就可以簡單地將出邊終點的權值++,繼續去加下一個點了。如果不包含權值x的邊,我們就需要新建一個點\(now\)並讓\(cur\)把邊連向他,\(now\)代表的長度自然是\(cur\)的長度+2,然后我們只要求出\(now\)\(fail\)就完事了。

\(fail\)的話可以用cur的\(fail\)來求,就用上面求\(cur\)的方法,但是不能用\(cur\)本身(想一想,為什么)

當然最后千萬不要忘記把\(last\)的值更新啊\(qwq\)

void extend(int x){
	int cur=getfail(last);
	int now=prt[cur].son[x];
	if(!now){
		now=++num;
		prt[now].len=prt[cur].len+2;
		prt[now].fail=prt[getfail(prt[cur].fail)].son[x];
		prt[cur].son[x]=now;
	}
	prt[now].siz++;
	last=now;
}

累計答案可以從下往上把回文串數目加起來,顯然上面的串一定是下面串的子串嘛\(qwq\)

void count(){
	for(int i=num;i>=2;i--)
		prt[prt[i].fail].siz+=prt[i].siz;
}

4.舉個栗子:

如圖,我們已經把串\(abab\)的回文自動機建好了,下面要添加一個點\(a\),此時\(last=5\)

首先求出\(cur\)\(last\)所代表的回文串\(bab\)前邊的字符正好與要加入的字符\(a\)相等,所以\(cur\)就是\(last\),我們發現\(cur\)不存在邊權為\(a\)的出邊,於是新建個點 6,從\(cur\)連一條邊\(a\)到 6;

6 的長度自然是5的長度+2\((a'bab'a)\)

然后求6的\(last\):5的\(fail\)指向3\((b)\),可以發現,3前面的那個字符\(a\)就是新加的字符(怎么那么巧...),於是我們把6的fail指向點3的\(a\)邊所指向的點4;

嗯,\(last\)更新為6,6的數量++,結束;

最后累加答案,

\(siz(6)=1\)\(siz(4)=1\)

\(siz(5)=1\)

\((siz(4)+=siz(6))=2\)

\((siz(3)+=siz(5))=2\)

\((siz(2)+=siz(4))=3\)

附:閑得自己也寫了個造圖的代碼。。。

void print(int x){
	if(cz[x]) return;
	cz[x]=1;
	printf("	%d->%d[style=\"dashed\"];\n",x,sam[x].link);
	for(int i=0;i<=25;i++)
		if(sam[x].ch.count(i))
			printf("	%d->%d[label=%d];\n",x,sam[x].ch[i],i),
			print(sam[x].ch[i]);
}
void Vz(){
	printf("digraph zhy{\n	rankdir = LR;\n");
	print(0);
	printf("}\n");
}

5.例題(Luogu-1659):

\(\quad\)這道題的話就是把這些點按照長度從大到小排一遍序,然后前\(k\)個奇數長的乘起來就是答案啦,注意這題k較大,還要用快速冪,代碼:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn=1e6+100,P=19930726;
struct node{
	int len,fail,son[26],siz;
	node(){
		len=fail=0;
		for(int i=0;i<=25;i++)
			son[i]=0;
	}
};
node prt[maxn]; 
int n,last,len,num;
ll ans=1,k;
char s[maxn];
ll poww(ll x,int y){
	ll base=1;
	while(y){
		if(y&1) base*=x,base%=P;
		x*=x,x%=P;
		y>>=1;
	}
	return base;
}
bool cmp(node x,node y){
	return x.len>y.len;
}
int getfail(int x){
	while(s[n-prt[x].len-1]!=s[n]) x=prt[x].fail;
	return x;
}
void extend(int x){
	int cur=getfail(last);
	if(!prt[cur].son[x]){
		int now=++num;
		prt[now].len=prt[cur].len+2;
		prt[now].fail=prt[getfail(prt[cur].fail)].son[x];
		prt[cur].son[x]=now;
	}
	prt[prt[cur].son[x]].siz++;
	last=prt[cur].son[x];
}
int main(){
	scanf("%d%d",&len,&k);
	scanf("%s",s);
	last=num=1,prt[1].len=-1;
	prt[0].fail=prt[1].fail=1;
	for(n=0;n<len;n++) extend(s[n]-'a');
	for(int i=num;i>=2;i--)
		prt[prt[i].fail].siz+=prt[i].siz,prt[prt[i].fail].siz%=P;
	sort(prt+1,prt+num+1,cmp);
	int now=1;
	while(k){
		if(now>num){
			printf("-1\n");
			return 0;
		}
		if(prt[now].len%2==0){
			now++;
			continue;
		}
		if(prt[now].siz<k){
			k-=prt[now].siz;
			ans*=poww(prt[now].len,prt[now].siz)%P;
			ans%=P;
			now++;
		}
		else{
			ans*=poww(prt[now].len,k)%P;
			ans%=P;
			k=0;
		}
	}
	printf("%lld\n",ans);
}





免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM