Loading

回文自动机(PAM)学习&练习笔记

好久没写博客了喵,其实是我最近太颓了,啥都没干。于是决定学点新东西防颓废。

学习笔记

感觉 PAM 对有 SAM 基础的人会很友好,你会发现这个东西比 SAM 好理解多了。

前置结论

一个字符串本质不同的回文串是 \(O(n)\) 级别的。

设原来有一个字符串 \(S\) (黑色部分),往后添加了一个字符 \(c\) (红色部分)。

我们假设蓝色字符与红色字符之间的字符串回文,且这个是以红色字符结尾的最长的回文串。

如果存在一个更短的回文串,比如绿色到红色之间的字符串。那么一定有:两个紫色区间的串相等,且都回文。

也就是说,每新加入一个字符至多产生一个新的本质不同的回文串。

PAM 的结构

  • 有两个根,一个是奇根,一个是偶根,奇根底下的点表示的是长度为奇数的点,偶根底下表示的是长度为偶数的点。

  • 和 trie 很像,但是每一个节点存的是一个回文串,表示的是从这个节点一直爬到根再回来的这个串。比如到奇根的路径是 abcd ,那么这个节点表示的就是 abcdcba

建立 PAM

一般采用增量法构造。

维护的值

注意,以下用 fa 表示 trie 树上的父亲。

  • tr[N][S] :trie 上的转移边。

  • fail[N] :指向每一个节点除自己外最长的回文后缀。为什么要维护这个呢?可以看看上面那个前置结论,可能会明白了。

  • len[N] : 每一个节点最长的回文串长度,根据 PAM 形态的定义,显然有 \(len(u)=len(fa(u))+2\)

构造方法

首先建立两个根:奇根,偶根。

我们考虑把偶根的 \(len\) 设为 \(0\) ,奇根的 \(len\) 设为 \(-1\) ,偶根的 \(fail\) 指向奇根。

\(len\) 这么开的原因很简单:\(len(u)=len(fa(u))+2\) ,偶根表示空串,而奇根底下的 \(len\) 会从 \(1\) 开始,方便很多。

\(fail\) 为什么这么开看完下面就懂了。

设我们要插入 \(S_i\)

\(S_{i-1}\) 所在的节点为 las,我们通过 las 以及它的 fail 等信息来找出当前插入的字符的位置。

考虑怎么找这个节点在 trie 上的父亲。

首先得要保证这个节点回文。

如果 \(S_{x,i}\) 回文,那么 \(S_{x+1,i-1}\) 也回文,所以我们直接遍历 \(las\) 所有的 fail ,找到最长的一个“合法”的回文串。

怎样的回文串合法呢?显然是两边都是 \(S_i\) 这个字符,也就是说,满足 \(S_{i-1-len(x)}=S_i\) 的节点 \(x\) 是我们要找的。不满足就跳 \(fail\)

会不会永远跳下去呢?这就是奇根 \(len=-1\) 的妙处了!发现沿着 \(fail\) 跳,总能跳到奇根上(这个也是偶根指向奇根的原因),因为必定有 \(S_{i-1+1}=S_i\) ,所以总会停止的。

于是我们找到了这个节点在 trie 树上的父亲 \(f\)tr[f][c] 就是这个节点。

还要维护 \(fail\)

因为 \(fail\) 不能指向自己,相当于指向次短的回文串,那么从 fail[f] 开始往上跳到一个“合法”的节点即可。

不要忘记更新 \(len(u)=len(f)+2\) 以及 \(las=u\),PAM就建完啦!

时间复杂度

发现每次 \(las\) 深度会增加 \(1\) ,每跳一次深度就会减少 \(1\) ,所以是线性的!

另一个有用的信息

这个做题很常用,trans 指针,指向 \(len\) 不大于当前节点 \(len\) 一半的节点。

维护方法和 \(fail\) 很像,从 \(trans(f)\) 开始不断跳 \(fail\) ,跳到满足 \(2*(len(x)+2)\le len(u)\) 并且 \(S_{i-len(x)-1}=S_i\) 的节点为止。

注意这里要加二!因为 trans 指向的应该是 \(x\) 的孩子。

还得注意特判 \(len(u)\le 2\) 的情况,这时候直接 \(trans(u)=fail(u)\) 即可。

复杂度证明和 \(fail\) 很像,还是线性的。

附上我第一代 PAM 板子。根据经验,板子会随时间变化而大型改变。。。

inline int getfail(int x,int i){
	while(i-len[x]-1<0||str[i]!=str[i-len[x]-1])x=fail[x];
	return x;
}
inline int gettrans(int x,int i,int lim){
	while(2*(len[x]+2)>lim||str[i]!=str[i-len[x]-1])x=fail[x];
	return x;
}
void build(char*str,int n){
	len[0]=0,len[1]=-1,fail[1]=0,tot=1,las=0;
	for(int i=0;i<n;++i){
		int c=str[i]-'a',f=getfail(las,i);
		if(!tr[f][c]){
			fail[++tot]=tr[getfail(fail[f],i)][c];
			tr[f][c]=tot;
			len[tot]=len[f]+2;
			if(len[tot]<=2)trans[tot]=fail[tot];
			else trans[tot]=tr[gettrans(trans[f],i,len[tot])][c];
		}
		las=tr[f][c];
	}
}

练习笔记

比 SAM 轻松多了!!! 没有大型DS,没有很长的代码,建 PAM 也比建 SAM 小清新的多!

容易发现,以 \(i\) 结尾的回文子串数量恰好为它所在节点在 fail 树上的深度,在新建节点的时候顺便维护深度即可。

找到 \(len(trans(u))\) 为偶数且 \(len(trans(u))*2=len(u)\)\(u\) ,取最大的 \(len(u)\) 即可。

维护以 \(i\) 结尾的最长回文串长度 \(a_i\),反过来再做一遍以 \(i\) 结尾的最长回文串长度 \(b_i\),取 \(\max\{a_i+b_{i+1} \}\) 即可。

直接建个 PAM, 提取所有 \(len\) 为奇数的节点,按照 \(len\) 降序排序。再同时维护每一个串的出现次数,这在 fail 树上打标记即可轻松维护,不会建议重修 ACAM 和 SAM。比 SAM 小清新的是,PAM 节点编号就是拓扑序,不用拓扑排序了。

刚看完题吓我一跳,如果可以建出 “广义PAM” ,对两个串分别打标记,在 fail 树上跑出每一个节点的回文串在两个串的出现次数,找到都出现的,取 \(\max\{len \}\) 不就完事了么?后来一想,广义PAM不比广义SAM好建的多?直接 \(las\) 设为 \(0\) 再跑一遍就好了,不会像广义SAM一样假掉。

上一题的双倍经验,改成每一个节点两个串出现次数相乘即可。

这题在当年应该不简单吧,SAM+倍增+manacher,虽然也不难想。但是PAM出来了之后,这个比模板还模板啊!!!

好题!
首先我们发现答案一定是经过一堆以二操作结尾的操作序列,然后不断一操作得到的。
考虑先建 PAM,设 \(dp(u)\) 表示变成 \(u\) 这个节点代表的回文串的最小步数,那么答案就是 \(\min\{dp(u)+n-len(u) \}\)
考虑怎么转移。我们发现能对答案产生贡献的串的长度一定是偶数(二操作完必然是偶数),所以我们把偶根丢进队列广搜偶树。

通过一操作转移到这个节点:\(dp(u)=dp(fail(u))+1\) ,可以考虑它的 \(fa\) 先在末尾插入这个字符再执行二操作(显然偶树每一个节点最后二操作一下最优)。
通过二操作转移到这个节点:\(dp(u)=dp(trans(u))+1+\dfrac{len(u)}{2}-len(trans(u))\) 。先用一操作填满到根的路径,再用一次二操作。

要看我的实现的话,可以到码库里面找~

posted @ 2021-03-05 08:59  zzctommy  阅读(332)  评论(0编辑  收藏  举报