回文自动机 PAM
PAM 定义
我们用PAM上的一个节点代表原序列的一个本质不同回文串。
由于回文串分奇和偶两个长度,我们开两颗树分别存储奇回文串和偶回文串,不妨称之为奇树,偶树。
由于回文串前后相等,我们定义一个点对应的回文串是:一次写下从它到它这棵树的根,再从根回到它的路径上所有边的字符,定义奇树的与根相连的边上的字符只书写一次,偶树书写两次。
比如以样例 debber 为例,设 \(0\) 是偶树的根,\(1\) 是奇树的根,它的PAM长这样:

在每个下标,我们存储三个东西:\((len,fail,ch)\),其中 \(ch\) 是子节点数组。
\(len\) 是指这个点代表的回文串长度,\(len_1=-1,len_0=0,len_i=len_{fa_i}+2\)。
fail 指针定义
定义 \(fail_i\) 指向代表节点 \(i\) 的这个回文串的最长回文后缀的节点。
举例:
abba的最长回文后缀是aaaaa是aaaacaca是aca
特别的,我们让 \(fail_0=1\)。
构建 PAM
考虑逐个加入字符,设加入了 \(i\) 个字符,现在加入第 \(i+1\) 个字符 \(c\)。
这个思想是很暴力的:
设 \(now\) 为 \([1,i]\) 的最长回文后缀所对应的节点(其实就是插入 \(i\) 后的节点)。
不断跳 \(now\) 的 \(fail\),直到跳到 \(now\) 这个所代表的串在原串往后一个字符是 \(c\) 并且往前一个字符也是 \(c\)(这个原串指的是右端点为 \(i\) 的情况下)。
如果现在 \(now\) 有 \(c\) 这个儿子,直接跳过去即可,否则新建一个节点作为它的儿子,并记录 \(len\) 这个信息,然后让它的 $fail $ 指针指向 \(now\) 继续跳 \(fail\) 跳到合法为止,让新节点的 \(fail\) 指向 \(now'\) 的对应儿子即可(不存在就是 \(0\))。
复杂度证明
- 空间:每次加入一个 \(i\) 至多新开一个节点,节点数 \(O(n)\)。
- 时间:考虑 \(now\) 的变化,每次往上跳都会导致 \(fail\) 树深度减少,它每插入一个元素自身深度最多增加 \(1\),因此至多总跳 \(2n\) 次,是 \(O(n)\) 的。
模板题
求PAM的时候顺带记一下fail树深度即可。
int dep[N],len[N],fail[N],ch[N][26],n,m,idx=1;
string a;
int find(int x,int i){
while(i-len[x]-1<=0||a[i]!=a[i-len[x]-1])x=fail[x];
return x;
}
signed main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>a;n=a.size();a=" "+a;
fail[0]=1;len[1]=-1;
for(int i=1,lt=0,now=0;i<=n;++i){
a[i]=(a[i]-97+lt)%26+97;int x=a[i]-'a';
now=find(now,i);
if(!ch[now][x]){
fail[++idx]=ch[find(fail[now],i)][x];
ch[now][x]=idx;len[idx]=len[now]+2;
dep[idx]=dep[fail[idx]]+1;
}
now=ch[now][x];
cout<<(lt=dep[now])<<" ";
// cout<<now<<" "<<dep[now]<<" "<<len[now]<<" "<<fail[now]<<"\n";
}
}

浙公网安备 33010602011771号