回文自动机学习笔记(配图)

前言

回文树(EER Tree,Palindromic Tree,也被称为回文自动机)是一种可以存储一个串中所有回文子串的高效数据结构。使用回文树可以简单高效地解决一系列涉及回文串的问题。

结构

回文树是由两个树组成。根节点分别是 0 0 0 − 1 -1 1,分别表示长度为偶数和奇数的回文串(存储方式和 trie 类似)。一棵表示 abba 的回文树长这样:

其中,黑色边表示 fail 指针。回文串的 fail 指针,指向「后缀最长回文串」。

转移

现在给定字符串 s s s,假设我们已经处理好了 [ 1 , p − 1 ] [1, p-1] [1,p1] 之前的回文树,现在要添加字符 s p s_p sp

假设存在 [ 1 , p − 1 ] [1, p-1] [1,p1] 的后缀回文串长度为 l e n len len,当且仅当 s p − l e n − 1 = s p s_{p - len - 1} = s_p splen1=sp 时,可以添加新的回文串。

我们可以不断地查询 fail 指针,假设查询到长度为 l e n len len 的回文串,那么直到 s p − l e n − 1 = s p s_{p - len - 1} = s_p splen1=sp 时,添加新的回文串 [ p − l e n − 1 , p ] [p - len - 1, p] [plen1,p]

现在我们要更新 [ p − l e n − 1 , p ] [p - len - 1, p] [plen1,p]fail 指针。我们回到刚刚长度为 l e n len len 的回文串,继续查询 fail 指针,直到继续存在 s p − l e n − 1 = s p s_{p - len - 1} = s_p splen1=sp 时,更新 fail 指针。

找到 A 所代表的节点,然后在下面增加一个 s p s_p sp,并且将 s p s_p sp 节点的 fail 指针指向 B。

Oi-wiki 上面有详细的证明证明时间复杂度是正确的 O ( n ) O(n) O(n)

实现

首先,我们要实现 getfail 函数,表示从 [ 1 , p − 1 ] [1, p-1] [1,p1] 到 A,和 A 到 B 的操作。

int getfail(int x, int i){
	// 表示节点编号为 x 中,满足第 s[i] = s[i - len - 1] 的最长子串 
	while(i - len[x] - 1 <= 0 || s[i - len[x] - 1] != s[i])	x = fail[x];
	return x; 
}

假如我们从第 i ( p ) i(p) i(p) 号位置插入新字符 c ( s p ) c(s_p) c(sp),有 insert 函数。

int insert(char c, int i){// 在第 i 位新增一个字符 c  
    int p = getfail(now, i);// 此时 p 是自动机中 A 的编号  
    if(!t[p][c - 'a']){// 新的子节点  
        fail[++tot] = t[getfail(fail[p], i)][c - 'a'];  
        t[p][c - 'a'] = tot;  
        len[tot] = len[p] + 2;  
//        cnt[tot] = cnt[fail[tot]] + 1;  
    }  
    now = t[p][c - 'a'];  
    return cnt[now];  
}  

例题 P5496 【模板】回文自动机(PAM) - 洛谷

代码如下,增加了 cnt 数组。

#include <bits/stdc++.h>
#define pii pair<int, int>
#define pb push_back
#define min(a,b) ((a)<(b)?(a):(b))
#define max(a,b) ((a)>(b)?(a):(b))
using namespace std;
using ll = long long;
const int N = 5e5 + 5;
string s;
int len[N], fail[N], t[N][26];
// 分别表示节点编号为 i 的长度、最长后缀回文串编号, 
int cnt[N], n, tot = 1, now;

int getfail(int x, int i){
	// 表示节点编号为 x 中,满足第 s[i] = s[i - len - 1] 的最长子串 
	while(i - len[x] - 1 <= 0 || s[i - len[x] - 1] != s[i])	x = fail[x];
	return x; 
}

int insert(char c, int i){// 在第 i 位新增一个字符 c 
	int p = getfail(now, i);// 此时 p 是笔记中 A 的编号 
	if(!t[p][c - 'a']){// 新的儿子 
		fail[++tot] = t[getfail(fail[p], i)][c - 'a'];
		t[p][c - 'a'] = tot;
		len[tot] = len[p] + 2;
		cnt[tot] = cnt[fail[tot]] + 1;
	}
	now = t[p][c - 'a'];
	return cnt[now];
}

signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>s;
	n = s.size();
	s = " " + s;
	fail[0] = 1, len[1] = -1;
	int lst = 0;
	for(int i = 1;i <= n;i++){
		if(i-1)	s[i] = (s[i] + lst - 97) % 26 + 97;
		lst = insert(s[i], i);
		cout<<lst<<' ';
	}
	return 0;
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值