回文树(回文自动机)学习笔记

学习回文自动机时感觉很多资料的思路都来得很陡,而且缺失了基本的原理证明。因此在这里重新整理了一下。


用途

可以求出每一个字符的“最长后缀回文”,也可以储存所有的回文串。


思维过程

现在我们要做的是:在对一个字符串逐个插入字符时,求得它与串内其余字符形成的新回文串。

观察可以发现,如果 \(x \sim i\) 形成回文串,那么 \(x+1 \sim i-1\) 必须为回文串。

对于任意一个已经确定是回文串的 \(x+1 \sim i-1\) 进行单次匹配,只需要比较 \(s[x]\)\(s[i]\) 是否相等即可。

对于新插入的节点 \(i\),找到 \(1 \sim i\) 的后缀的最长回文,模仿 KMP 的方式,进行如下操作:先试着和 \(1 \sim i-1\) 后缀的最长回文匹配。如果已经成功匹配了那不用说;如果失败了,就试着与 \(1 \sim i-1\) 后缀的次长回文匹配,还不行就与 \(1 \sim i-1\) 后缀的次次长回文匹配,直到匹配成功或后缀已经是空串了。

那么,是不是可以像 KMP 那样,对于每一个节点 \(i\),记录一下它后缀的最长、次长回文分别的位置,就可以快速在这些后缀间跳跃了?可以发现,这其实是不能直接实现的。最主要的问题是“次长的次长”的位置并没有在先前被计算过,更本质的原因是,它似乎并不是任何节点的最长后缀回文

但其实我们可以证明,某个节点的“次长”,一定与先前某个节点的“最长”完全相同。

证明:

来观察一下这幅图:

白色的矩形为 \(1 \sim i-1\) 的最长后缀回文,而右侧的红 - 黄矩形为次长的。

根据回文串的对称性,易得红 - 黄矩形的左侧有一个和它镜像对称的红 - 黄矩形;又由于回文串的镜像还是它本身,左侧的的矩形和右侧的矩形完全相同

但是这时我们依旧不能确保左侧矩形它的右端点的“最长”。不过,如果它不是最长的话,就又回到了上图中同样的情景:原本的左侧矩形变成了现在的右侧矩形,而又绝对有一个新的左侧矩形与之完全相同。如此递归往复,总可以保证红 - 黄矩形是某个节点的“最长”。

证毕。

很显然,当两个子串完全相同时,就说明它们的最长回文后缀完全相同。也就是说,如果我们知道最开始那个完全相同的“最长”,知道它的“次长”,就可以找到当前这个“次长”的次长。如此,次长的次长、次长的次长的次长等等都可以顺利求出,便可以模仿 KMP 的“跳后缀”过程了。

那如何求出那个相等的最长回文后缀呢?引入一个数据结构——字典树。字典树显然是非常适合干这件事的:它可以快速查询一个相同的字符串,还很容易组织众多的字符串。

注意,这里节点一旦被插入到字典树中,它代表的就不是自己原本在字符串中的下标了——它代表的是一个回文串本身


算法实现

有了刚刚的分析,算法的实现就很明了了。

  • 字典树的建立

    这里的字典树比较特殊。首先,我们得建两棵字典树——因为回文串有两种,长度为奇数以及长度为偶数的。

    节点 0 代表偶回文树的根,一个回文串的读法为“从当前节点开始,往上读到根节点,再从上往下读回当前节点”;节点 1 代表奇回文树的根,一个回文串的读法为“从当前节点开始,往上读到根节点,与根节点相连的边只读一次,再从上往下读回来”。下图的蓝边代表的就是字典树的普通边。

  • fail 指针

    fail 指针就是方才所提到的“指向次长后缀回文串”的指针,也是下图的黑边。容易发现,fail 的连接也构成了一棵树

    初始化 0 节点的 fail 指向 1,1 节点的 fail 无所谓(但如果你仔细观察了下图,会发现以 1 为父亲的节点的 fail 都是 0,这在后面的代码里会特判处理)。

  • len

    len 代表回文串的长度,这是树中每个节点还需要存储的一个信息。这里有一个小技巧:将节点 1 的 len 设置为 -1,节点 0 的 len 设置为 0,这样每个节点的 len 就是其父节点的 +2。

image

(图片引自某位人士的博客,我忘了是谁了。)

  • 具体过程:把字符串中的节点逐个插入字典树。对于节点 \(i\)

    • 记录 \(i-1\) 的最长后缀回文在回文树中的对应节点 lst。从 lst 开始,不断跳 fail 指针,直到找到一个节点 \(p\),使得 \(s[i] = s[i-len_p-1]\)

    • 新建一个节点代表 \(i\) 的最长后缀回文,接到 \(p\) 的一个儿子指针下。注意,如果 \(p\) 已经有对应的儿子了,就像正常的字典树那样,我们就不用再新建了,下面一步“找 fail” 也不用进行了。

    • 找到了 \(i\) 的“最长”,接下来要求“次长”——也就是 fail 了。只需要从 \(fail_p\) 开始接着跳 fail,直到又找到一个节点 \(q\) 满足 \(s[i] = s[i-len_q-1]\)\(q\) 就是 \(i\) 的 fail 了。

    • 更新一下 lst。

ok,show you the code.(其实很短)

题目:P5496 【模板】回文自动机(PAM)

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 5e5+5;
int n, tot = 1;
char str[MAXN];

struct Trie{
	int fail, len, sum, ch[26];
} tree[MAXN];

inline int Get_fail(int pt, int i){
	while(str[i] != str[i-tree[pt].len-1])	pt = tree[pt].fail;
	return pt;
}

int main(){
	scanf("%s", str+1);
	n = strlen(str+1);
	tree[0] = (Trie){1, 0, 0};
	tree[1] = (Trie){0, -1, 0};
	for(int i = 1, lst = 0; i <= n; i++){
		str[i] = (str[i]-97+tree[lst].sum)%26+97;
		int p = Get_fail(lst, i);
		if(!tree[p].ch[str[i]-'a']){
			++tot;
			//注意以下两句的顺序关系。其实这里隐含了一个特判,即以 1 为父亲的节点,它们的 fail 应指向 0。
			tree[tot].fail = tree[Get_fail(tree[p].fail, i)].ch[str[i]-'a'];
			tree[p].ch[str[i]-'a'] = tot;
			tree[tot].sum = tree[tree[tot].fail].sum+1;
			tree[tot].len = tree[p].len+2;
		} 
		lst = tree[p].ch[str[i]-'a'];
		printf("%d ", tree[lst].sum);
	}
	
	return 0;
}

复杂度证明

这个我不太清楚,大概是用势能之类的搞一下,网上肯定有很多说得清楚的资料。反正是 \(O(n)\) 的。


例题

posted @ 2024-02-01 12:53  David_Mercury  阅读(20)  评论(0编辑  收藏  举报