回文自动机PAM从菜到菜

回文自动机

基础操作

两个初始状态一个长度为 0,1 的偶回文根和奇回文根。

转移 δ(x,c)x 节点代表的回文串转移到两端加入字符 c 后到达的节点。

fail(x) 指针,指向自身的后缀最长回文串。特殊地,偶回文根指向奇回文根,奇回文根没有 fail 指针,因为奇回文根不会失配。

怎么构建呢,考虑增量构建法。

加一个字符,假设当前加入的是第 p 个字符。

我们从上一次到达的点,一直跳 fail ,找一个边上可以扩展的一个点停下,也就是第一个满足 s[plenx1]=s[p] 的点 x。看它是否有 δ(x,s[p]) 的转移,有就不管,没有就新建节点。新建节点的 fail 指针通过 x 再跳一次 fail 找到第一个满足前面条件的点确定。

为什么从上一次插入时到达的点开始?因为它是以 p1 为结尾的一个尽可能长的回文串。为什么只用改第一个满足条件的点?这是因为后面的满足条件的点必定在之前出现过,这个我们稍晚一点给出证明。

关于为什么暴力跳 fail 的时间复杂度有保证,这是因为我们每次插入时的起点是上一次跳到的点。每次我们跳一次 fail ,在 fail 树上的深度至少减一,而连接一个 fail 只会深度加一,可得复杂度线性。

关于插入时后面的满足条件的点必定出现过的证明。因为后面的点必定为前面的点的后缀,这些点又是回文,所以它前面肯定出现过。以及,状态数是线性的,这个可以通过刚才说的进行归纳证明。因为转移的节点唯一,然后转移数同样是线性的。

边界情况,偶根作为 0 的话可以解决一起。

【模板】回文自动机(PAM)

#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			T p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p/=10;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
	template<class T>
	inline void print(T x){
		if(x<0) {x=-x;pc('-');}
		static char st[40];
		static int top;
		top=0;
		do{st[++top]=x-x/10*10+'0';}while(x/=10);
		while(top) {pc(st[top--]);}
	}
	template<class T,class ...A>
	inline void print(T s,A ...a){
		print(s);print(a...);
	}
};
using IO::read;
using IO::print;
const int N=5e5+3;
int n;
char la_ans;
struct Pali_auto{
	int len[N+2],fail[N+2];
	int tr[N+2][26];
	ll cnt[N+2];
	char s[N];
	int tot,la,tip;
	inline int newnode(int L){
		len[++tot]=L;
		memset(tr[tot],0,sizeof(tr[tot]) );
		fail[tot]=cnt[tot]=0;
		return tot;
	}
	inline void build(){
		tot=-1;
		la=tip=0;
		newnode(0);
		newnode(-1);
		fail[0]=1;
	}
	inline int extend(int x){
		while(s[tip-len[x]-1]!=s[tip]){
			x=fail[x];
		}
		return x;
	}
	inline ll ins(char ch){
		s[++tip]=ch;
		int c=ch-'a';
		int x=extend(la);
		if(!tr[x][c]){
			int y=newnode(len[x]+2);
			fail[y]=tr[extend(fail[x])][c];
			cnt[y]=cnt[fail[y] ]+1;
			tr[x][c]=y;
		}
		la=tr[x][c];
		return cnt[la];
	}
}t;
char s[N];
int main(){
	file(a);
	scanf("%s",(s+1) );
	n=strlen(s+1);
	t.build();
	for(int i=1;i<=n;++i){
		//pc(s[i]);
		ll k=t.ins(s[i]);
		printf("%lld ",k);
		s[i+1]=(s[i+1]-97+k)%26+97;
	}
	return 0;
}

一点性质

性质1

回文串的 border 是回文串。其回文后缀为其 border

证明:

s 为回文的前提下,其 border 相等且互为反串,得证。

性质2

一个串 s 的半长(及以上)的 border 为回文串,则 s 为回文串。

证明:

达到半长可覆盖全串,易证。

性质3

回文串 xyx 的最长回文真后缀,zy 的最长回文真后缀, u,v 满足 x=uy,y=vz,则:

1.|u||v|

2.若 |u|>|v| ,则 |u|>|z|

3.若 |u|=|v|u=v

证明:

1.显然 |u|x 的最小周期,|v|y 的最小周期。由 yxborder 兼后缀,|u| 也为 y 的周期。若 |u|>|v| ,则 |v|y 的最小周期不成立,矛盾。因此 |u||v|

2.首先 vx 前缀,设字符串 w 满足 x=vw ,则 zwborder (由 y=vz)。假设 |u||z| ,则 |zu|=|w|2|z|,则 w 为回文串,wxborder。再由 |u|>|v| ,得 |zu|=|w|>|vz|=|y|,与 y 为最长回文后缀相矛盾。所以 |u|>|z|

3.因为 u,v 皆为 x 的前缀,且长度相等,因此 u=v

性质4

字符串 s 的所有回文后缀按长度排序后可划分为 O(log|s|) 组等差数列。

证明:

对于 s 的最长回文后缀的 border 使用 border理论性质5 即可。

或者用性质3可知每次相邻的三个回文后缀长度,最小的长度必定小于最大的长度的一半。

应用

本质不同回文子串个数

除去初始状态后的状态数量。

回文子串出现次数

fail 树上做个 topo 排序累加一下子树中串出现次数即可。

最小回文划分

问最少将字符串 s 划分成多少个部分使得其各部分回文。

f[i] 表示前缀 s[1,i] 的最小回文划分,朴素的DP转移式是:(pali 为回文缩写)

f[i]=mins[j+1,i]is palif[j]+1

回文子串个数是 O(n2) 的,直接做会寄。

考虑回文后缀会划分成 O(log|s|) 个等差数列。我们可以维护整个等差数列的 f 贡献和。记 g[x] 为从 x 开始的等差数列贡献和,dif[x] 表示 len[x]len[fail[x]]slink[x]xfail 父亲链上第一个满足 dif[p]dif[fail[p]]p。我们的等差数列的贡献不包括 slinkslink 的贡献放到下一个等差数列中计算。

首先知道一个结论,若 xfail[x] 位于同一等差数列,那么 fail[x] 的上一次出现位置为 idif[x],这个结论我们等会给出证明。那么你发现 g[fail[x]] 保存的贡献只比 g[x] 存的少一个。因为它原来保存的贡献位置只差为 dif[x] ,而现在 fail[x] 向后移动了 dif[x] 并加上了 x 的贡献,所以 x 把原来平移走的贡献补上,多出来的是 g[fail[x]] 中最后一个贡献平移之后的结果,这个贡献是 f[idif[x]len[slink[x]]]。这么干讲可能比较抽象,我把 OI-wiki 那个图抽过来。

br

大概就是这样?

关于那个结论:“若 xfail[x] 位于同一等差数列,那么 fail[x] 的上一次出现位置为 idif[x] ” 的证明。

啊,首先 fail[x] 肯定是在 idif[x] 出现过的。只要证明 fail[x](idif[x],i) 中不曾出现即可。考虑反证,假设 fail[x] 在区间 (idif[x],i) 出现过,由于他们属于同一等差数列(这里指 dif 相等)有 2|fail[x]||x| ,显然 fail[x] 与前面的 idif[x] 位置的 fail[x] 有交。记交集为 ab 满足 ab=fail[x] 。显然 aba 为回文串,这个比 fail[x] 长,与 fail[x]x 的最长回文后缀矛盾,因此得证。

posted @   cbdsopa  阅读(48)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示