回文树/自动机 (Palindromic Tree/Automaton)
回文树/自动机 (Palindromic Tree/Automaton)
回文树, 又称回文自动机 (PAM), 用来解决字符串的回文子串问题.
推荐先学 后缀自动机, AC 自动机, 三者会有很多相似之处, 学起来会更加愉快. 而 Manacher 的内容一点也没有用到, 无需为了学 PAM 特意学习.
定义
形态上是两棵树 + .
每个节点表示一个本质不同的回文子串. 该串长度即为该节点长度.
树边带权, 是自动机的转移边. 权值为一个字符 , 表示起点两端同时加 得到的新串存在并且作为另一个回文子串.
因为奇数长度的节点只能转移到奇数长度的节点, 所以存在两个根, 分别作为奇树和偶树的根.
边向长度小的节点转移, 表示该点最大的回文后缀. 可以在两树之间连接.
建立一个数组 , 其中 表示原字符串中以第 个字符为结尾的最长的回文子串的节点位置. 由于 的 是 的最长回文后缀. 所以通过 所在的 链可以访问以任意字符为结尾的的回文后缀.
构造
一开始是空串, 只有两个根, 偶根 连奇根. 考虑将一个字符 加入到已经构建回文自动机的 的后面会在哪里出现回文串.
如果原来 的后缀是回文串, 从 往 链上跳. 如果 有回文后缀为 , 满足 , 则出现回文串 , 新建这个节点, 连接转移 , 将 指向 所在节点.
这时就有人要问了, 如果有多个节点需要新建呢? 但是再审视一下回文后缀的性质, 每次新建的点真的不止一个吗?
如果 的最长回文后缀是 , 仍存在一个比 短的回文后缀 . 因为 是回文串, 所以 也是一个和 本质相同的回文串. 所以不存在第二个新回文子串. 也就是说, 每个字符 的加入最多带来一个新节点.
接下来考虑 的连接, 设节点 在 的 上, 则 是 的后缀. 只要 , 那么 就是 的后缀.
只要判断 是否是 的后缀即可 (当然 要有转移 , 否则根本不存在 对应的节点). 在跳 的时候, 判断 ( 的转移 指向的节点) 的左端字符 是否和 对应位置的字符对应 (即判断是否有 成立). 如果到最后没有找到合法的 , 连向偶根.
以此类推, 将每个字符都插入后, 便构造了一个回文自动机.
下面对复杂度进行证明, 同样是将字符集规模看作常数.
空间复杂度
因为一个节点只有一个树上入边, 一个 出边. 空间复杂度取决于节点数, 每个节点和一个本质不同的回文子串一一对应, 只要分析本质不同的回文子串数量即可. 因为一共有 个字符, 之前已经说明, 每个字符的加入最多新出现一个本质不同的回文串, 所以最多有 个本质不同的回文串. 空间复杂度为
时间复杂度
除了跳 的 链, 其它部分都是显然的线性复杂度, 所以着重分析跳 的复杂度.
在 的 链上的满足以上条件的节点, 可能是 . 这样一来, 的 只要连向 , 就能使 的 链长比 的 链长大 .
每次跳 , 的 链长度都会减少 . 而每次最多在一个 不跳的情况下使 链比上一个 链长 . 所以总的跳 的总次数是线性的.
所以总复杂度是
模板
求一个长度为 的字符串的每一个前缀的回文后缀数量. ()
前缀 的回文后缀数也就是 所在的 链去掉两根的长度. 建立 PAM, 记忆化搜索统计长度即可.
代码
缺省源省略, fread()
需要 <cstdio>
unsigned m, n, Cnt(0), Ans(0), Tmp(0), Key;
bool flg(0);
char a[500005];
struct Node {
Node *Link, *To[26];
int Len;
unsigned int LinkLength;
}N[500005], *Order[500005], *CntN(N + 1), *Now(N), *Last(N);
int main() {
fread(a + 1, 1, 500003, stdin);
n = strlen(a + 1);
N[0].Len = -1;
N[1].Link = N;
N[1].Len = 0;
Order[0] = N + 1;
for (register unsigned i(1); i <= n; ++i) {
if(a[i] < 'a' || a[i] > 'z') {
continue;
}
Now = Last = Order[i - 1];
a[i] -= 'a';
a[i] = ((unsigned)a[i] + Key) % 26;
while (Now) {
if(Now->Len + 1 < i) {
if(a[i - Now->Len - 1] == a[i]) { // 符合左端字符对应位置是 c
if(Now->To[a[i]]) { // 有转移 c, 不新建节点, 只记录 Order
Order[i] = Now->To[a[i]];
flg = 1; // 标记表示本轮没有节点被新建
}
else {
Now->To[a[i]] = ++CntN; // 转移
CntN->Len = Now->Len + 2; // 长度 +2 (左右两端加 c)
Order[i] = CntN; // 记录 Order
}
break;
}
}
Last = Now; // 记录上一个节点, 优化下一次跳 Link 链的次数 (下一次跳是找 Order_i 的 Link)
Now = Now->Link; // 跳 Link
}
if(!flg) { // 有新节点, 连接这个点的 Link
Now = Last;
while (Now) {
if(Now->To[a[i]]) { // 有转移 c
if(Now->To[a[i]]->Len < Order[i]->Len) {// 长度合法
if(a[i - Now->Len - 1] == a[i]) { // 该节点左端包含于 Order_i->Link 的后缀
Order[i]->Link = Now->To[a[i]];
Order[i]->LinkLength = Now->To[a[i]]->LinkLength + 1;
break; // 找到 Link
}
}
}
Now = Now->Link; // 跳 Link
}
if(!Now) { // 无合适的 Link, 连向偶根
Order[i]->Link = N + 1;
Order[i]->LinkLength = 1;
}
}
else { // 有标记说明无新节点, 清空标记
flg = 0;
}
Key = Order[i]->LinkLength;
printf("%d ", Key);
}
return Wild_Donkey;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具