前言:
回文自動機($PAM$),也叫回文樹
可以用 $O(n)$ 的時間復雜度求出一個字符串的所有回文子串
本蒟蒻是學了兩遍才學明白的,這里推薦一下B站上關於回文自動機的講解
當然如果不方便看視頻的話,也可以看一下我自己關於回文自動機的一些理解
正文:
節點含義
類比 $AC$ 自動機每個節點的含義
回文自動機每個節點的含義表示在它的父節點兩側各加上一個兒子字符
奇根偶根
由於回文串有奇數長度和偶數長度兩種
所有我們的回文自動機自然會有兩個根——奇根和偶根
偶根的節點編號為 0,所代表的回文串的長度為 0,$fail$ 指針指向奇根
奇根的節點編號為 1,所代表的回文串的長度為 -1,$fail$ 指針指向自身(其實無所謂)
$fail$ 指針
我們再來說一下 $fail$ 指針的含義
一個節點的 $fail$ 指針,指向的是這個節點的最長回文后綴
所以在新加入一個字符的時候,我們要從當前節點不斷的跳 $fail$ 指針
直到跳到某一個節點所表示的回文串的兩側都能擴展一個待添加的字符
我們就看這個節點有沒有這個兒子,如果有就直接走下去,沒有就新建一個節點
新建節點的長度等於這個節點的長度加上 2(因為是回文串,要在兩側各擴展一個字符)
那每個節點的 $fail$ 指針要怎么求那
我們可以考慮一個節點的最長回文后綴
必然是在它父節點的某個回文后綴兩側各拓展一個當前字符得到的
所以新建一個節點之后,我們可以從它父親的 $fail$ 節點開始,不斷的跳 $fail$ 指針
直到跳到第一個兩側能拓展這個字符的節點為止,那么該節點的兒子就是新建節點最長回文后綴
之后我們再看一下奇根和偶根的 $fail$ 指針,由於奇根的子節點表示的回文串長度為 1,也是就該字符本身
所以奇根相當於是可以向兩側擴展任意字符的,所以我們把偶根的 $fail$ 指針指向奇根
而如果跳到了奇根,一定能向兩側擴展,所以奇根的 $fail$ 指針自然就無所謂了
代碼實現起來非常的簡單
char s[maxn]; int cnt,last;//cnt表示節點數,last表示當前節點 int sum[maxn];//統計每個回文串的出現次數 int son[maxn][26];//每個節點的兒子 int len[maxn],fail[maxn];//len表示當前節點回文串的長度,fail如上所述 int new_node(int length)//新建一個節點 { len[++cnt]=length; return cnt; } int get_fail(int pre,int now)//跳fail指針 { while(s[now-len[pre]-1]!=s[now]) pre=fail[pre]; return pre; } void build_PAM() { cnt=1,last=0;//奇根編號為1,其他節點從2開始 len[0]=0,len[1]=-1;//初始化,如上所述 fail[0]=1,fail[1]=1;//初始化,如上所述 for(int i=1;s[i];i++) { int cur=get_fail(last,i);//從當前節點開始,找到可擴展的節點 if(!son[cur][s[i]-'a'])//沒有這個兒子 { int now=new_node(len[cur]+2);//新建節點 fail[now]=son[get_fail(fail[cur],i)][s[i]-'a'];//找到最長回文后綴 son[cur][s[i]-'a']=now;//父子相認 } sum[last=son[cur][s[i]-'a']]++;//順帶求出每個回文串的出現次數 } }
應用
如果要求本質不同的回文串的個數,直接輸出 $cnt-1$ 即可(除去奇根)
如果要統計每個回文串的出現次數,還要從葉子節點向根遍歷一遍
因為我們當時統計回文串時只統計了完整的回文串,但並沒有記錄它的子串
所以我們要按照拓撲序將每個節點的最長回文后綴的出現次數加上該節點的出現次數
這樣我們就得到了一個字符串的所有回文子串的出現次數
for(int i=cnt;i>=2;i--) sum[fail[i]]+=sum[i]
另外,回文自動機還有一種常見操作就是在構造的時候順帶求出一個 $trans$ 指針
$trans$ 指針的含義是小於等於當前節點長度的一半最長回文后綴,求法和 $fail$ 指針的求法類似
當我們新建一個節點后,如果它的長度小於等於 2,那么這個節點的 $trans$ 指針指向它的 $fail$ 節點
否則的話,我們同理從它父親的 $trans$ 指針指向的節點開始跳 $fail$ 指針
直到跳到某一個節點所表示的回文串的兩側都能擴展這個字符
並且拓展后的長度小於等於當前節點長度的一半
那么新建節點的 $trans$ 的指針就指向該節點的兒子
for(int i=1;s[i];i++) { int cur=get_fail(last,i); if(!son[cur][s[i]-'a']) { int now=new_node(len[cur]+2); fail[now]=son[get_fail(fail[cur],i)][s[i]-'a']; son[cur][s[i]-'a']=now; //順帶求出trans指針 if(len[now]<=2) trans[now]=fail[now]; else { int tmp=trans[cur]; while(s[i-len[tmp]-1]!=s[i]||((len[tmp]+2)<<1)>len[now]) tmp=fail[tmp]; //拓展后的長度為len[tmp]+2 trans[now]=son[tmp][s[i]-'a']; } } last=son[cur][s[i]-'a']; }
有了 $trans$ 指針之后,我們就可以很輕松的切了這道雙倍回文(題目來自洛谷P4287)
后序:
關於馬拉車($Manacher$)算法
它可以 $O(n)$ 得到一個字符串以任意位置為中心的回文子串
而且我們的回文自動機好像並不能取代它,比如它的模板題(題目來自洛谷P3805)
然而本蒟蒻對這個算法還不是很熟,所以有關它的總結,可能要很久~很久~以后才會寫了