回文树(回文自动机) 学习笔记
回文树(回文自动机) 学习笔记
回文自动机可以存储一个字符串的所有回文子串,根据 Manacher 的过程我们可以知道一个字符串的回文子串个数是 \(O(n)\) 的。
结构
回文自动机的每个节点表示以某个位置结尾的最长回文子串。
回文自动机有两种边,转移边 \(tr\) 和失配边 \(fail\)。一条字符 \(c\) 的转移边到达的节点,表示当前节点的回文串前后各加一个字符 \(c\) 形成的字符串。失配边表示当前回文子串的最长回文后缀。
这样,我们从一个位置结尾的最长回文子串一直跳失配边即可遍历以这个位置结尾的所有回文串。
每个点还要存储回文串长度 \(len\)。
有两个初始节点,奇根和偶根,从奇根出发的就是奇回文串,从偶根出发的就是偶回文串。它们的 \(len\) 分别为 \(-1,0\),这样使得每条边连向的新点 \(len\) 都 \(+2\)。
构建
除了奇、偶根的 \(len\) 初始为 \(0,-1\) 外,还要让偶根的 \(fail\) 为奇根,这样当偶根也失配时就会找到奇根,而奇根是不会失配的(单个字符一定是奇回文串)。
接下来插入字符 \(p_i\)。我们维护 \(last\) 表示 \(p_{i-1}\) 结尾的最长回文串。初始 \(x=last\),我们一直跳 \(x\) 的 \(fail\) 直到当前串前后可以拼接一个 \(p_i\),则此时 \(tr[x][p_i]\) 即为 \(p_i\) 的最长回文串。如果不存在这个点,我们要执行下面的建点操作。
建点操作:给新点的编号加一后,我们要找到新点的 \(fail\)。令 \(y=fail[x]\),我们一直跳 \(y\) 的 \(fail\) 直到当前串前后可以拼接一个 \(p_i\),则此时新点的 \(fail\) 为 \(tr[y][p_i]\)。其中 \(tr[y][p_i]\) 一定存在,因为 \(y\) 是回文串 \(x\) 的一个后缀,它在 \(x\) 的前面还出现过一次,则 \(p_i+y+p_i\) 一定在前面还出现过一次。
注意如果新点的回文串为单个字符,要注意边界情况保证 \(fail\) 为偶根,否则可能出现 \(fail\) 指向自己的情况。
最后更新 \(last\gets tr[x][p_i]\)。
可以证明,时间复杂度是 \(O(n)\)。
代码
const int N=5e5+5;
int n;
char s[N];
int tot,fail[N],tr[N][26],len[N];
signed main(){
	read(s+1),n=strlen(s+1);
	len[1]=-1;
	len[2]=0,fail[2]=1;
	int last=2;
	fo(i,1,n) {
		int c=s[i]-'a';
		int x=last;
		while(s[i-len[x]-1]!=s[i]) x=fail[x]; //第一次跳失配
		if(!tr[x][c]) {
			tr[x][c]=++tot,len[tot]=len[x]+2;
			if(len[tot]==1) fail[tot]=2; //特判防止自指
			else {
				int y=fail[x];
				while(s[i-len[y]-1]!=s[i]) y=fail[y]; //第二次跳失配
				fail[tot]=tr[y][c];
			}
		}
		last=tr[x][c]; //记得更新 last
	}
	return 0;
}

                
            
        
浙公网安备 33010602011771号