迴文自動機 (PAM,Palindrome Automaton)
迴文自動機 (PAM,Palindrome Automaton)
如果學習了\(\text{AC}\)自動機和字尾自動機(\(\text{SAM}\)),那麼這個冷門演算法其實非常簡單
約定:原字串為\(S\),長度為\(|S|\)
結構介紹
自動機節點意義: \(\text{PAM}\)沒有複雜的結構,每個節點對應了一種迴文子串,節點個數\(\leq |S|+2\)
自動機的轉移:\(\text{PAM}\)和\(\text{AC}\)自動機一樣,有失配指標\(fail\)和匹配陣列\(nxt\)
\(fail_i\)即是\(i\)的字尾的最長狀態,\(i\)和\(fail_i\)的邊構成了兩棵樹
每個轉移\(nxt_{i,j}\)意味著在當前狀態\(i\)的串兩邊增加字元\(j\)
但是由於\(\text{PAM}\)的構造是一個線上演算法,所以如果想要像\(\text{AC}\)自動機一樣每次轉移直接訪問\(nxt\),需要結束後遍歷結構
構造
為了便於訪問,設偶數/奇數樹的根分別為\(0,1\),每個節點儲存一個當前狀態的長度\(len\),特別的,\(len_0=0,len_1=-1\),便於讓所有的串都滿足\(len_{nxt_{i,j}}=len_i+2\),同時偶數的根可以失配到奇數的根,即\(fail_0=1\),(實際這樣操作後就只有一棵樹了)
為了線上構造方便,\(\text{PAM}\)需要實現一個匹配函式\(\text{Find}(x,y)\),意思是在當前\(x\)狀態找到下一個位置\(S_y\)的匹配狀態,如果失配則返回奇數根\(1\)
int Find(int x,int y){
while(s[y]!=s[y-len[x]-1]) x=fail[x]; // 如果失配到了x=1,那麼必然有s[y]=s[y]
return x;
}
增加一個節點\(S_i=c\)
首先找到一個最長的匹配,設當前字首最長的迴文字尾對應的狀態為\(now\),則直接為\(now\)匹配\(S_i\)即可
然後是新建狀態(如果當前的迴文子串還未出現過)
和\(\text{AC}\)自動機類似,訪問\(fail\)樹上最近的匹配即可得到這個點的\(fail\)
需要注意的點是,因為當前節點可以是根節點,尋找\(fail\)必須在新建轉移\(nxt_{now,c}\)之前進行,否則可能找到的\(fail\)是自己
void Extend(int i,int c){
now=Find(now,i);
if(!nxt[now][c]) {
fail[++cnt]=nxt[Find(fail[now],i)][c];
len[nxt[now][c]=cnt]=len[now]+2;
}
now=nxt[now][c];
}
模板程式碼如下:
char s[N];
struct Palindrome_Automaton{
int len[N],fail[N],nxt[N][26],now,cnt;
void Init(){
rep(i,0,cnt) memset(nxt,fail[i]=0,104);
s[now=0]=len[1]=-1;
fail[0]=fail[1]=cnt=1;
}
int Find(int x,int y){
while(s[y-len[x]-1]!=s[y]) x=fail[x];
return x;
}
void Extend(int i,int c){
now=Find(now,i);
if(!nxt[now][c]) {
fail[++cnt]=nxt[Find(fail[now],i)][c];
len[nxt[now][c]=cnt]=len[now]+2;
}
now=nxt[now][c];
}
};
拓展:迴文串與\(\text{Border}\)
推論1:迴文串的\(\text{Border}\)也是迴文串
若有迴文串\(S\)的一個\(\text{Border} :T\),則\(S_{1,|T|}=S_{|S|-|T|+1,|S|}=reverse(S_{1,|T|})\)
故\(T\)也是一個迴文串
推論2:遍歷迴文自動機的\(fail\)鏈,能得到當前串的所有\(\text{Border}\)(基於推論1得到)
結合\(\text{kmp,AC}\)與\(\text{Border}\)的關係能夠有更好的理解