回文自动机(回文树)学习笔记
2022/8/30 23:26:15
本文主要是介绍回文自动机(回文树)学习笔记,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
回文自动机(回文树)学习笔记
前置知识
建议提前学习 Manacher 算法 和其他任何一种自动机,方便理解,不过不学问题应该也不大。
定义
回文自动机(PAM),也称回文树,是存储一个字符串所有回文子串的数据结构。
PAM 由转移边和后缀链接构成,它的每一个状态都代表着一个回文子串。
设 \(u\stackrel{c}{\to}v\) 表示有一条从状态 \(u\) 到状态 \(v\) 的转移边 \(c\),它的含义是:在状态 \(u\) 代表的回文子串两边都加一个字母 \(c\),可以得到状态 \(v\) 代表的回文子串。
设 \(\operatorname{fail}(u)\) 表示状态 \(u\) 的后缀链接,它的含义是:状态 \(u\) 代表的回文子串的最长回文真后缀对应的状态。
为了方便,我们对于每个状态 \(u\),记录它代表的回文子串的长度 \(\operatorname{len}(u)\)。
在 Manacher 算法中我们知道,回文串长度有奇数有偶数,这是不好处理的。当时我们采用的方法是在相邻两个字符之间插分隔符,把所有回文串都变成奇回文。这里我们换一种方法,建两个状态当根:一个 \(\operatorname{len}\) 为 \(-1\),子树为所有奇回文,称为奇根;一个 \(\operatorname{len}\) 为 \(0\),子树为所有偶回文,称为偶根。
特别地,\(\operatorname{fail}(0)=-1\),而 \(\operatorname{fail}(-1)\) 不重要,因为永远不会失配(左右各加一个字母得到的字符串只有一个字母,显然是回文串)。
例如,\(\texttt{abbab}\) 的 PAM 如下:(粉色边是后缀链接)
构建
考虑类似 SAM 的方式,每次在结尾插入一个字符,构建 PAM。
假设已经构建好前 \(i-1\) 个字符的 PAM,记最长回文后缀对应的状态为 \(lst\),现在要插入第 \(i\) 个字符。
我们从 \(lst\) 开始,不断跳后缀链接,直到一个状态 \(u\) 满足 \(s_i=s_{i-\operatorname{len}(u)-1}\)(记为 \(u=\operatorname{jump}(lst,i)\)),此时我们在状态 \(u\) 代表的回文子串前后添加字符 \(s_i\) 依然是回文子串,而且是新字符串的最长回文后缀。
如果状态 \(u\) 不存在 \(s_i\) 转移边,我们需要添加新状态 \(v\)。新状态 \(v\) 的后缀链接应当为 \(\operatorname{fail}(v)=w:\operatorname{jump}(\operatorname{fail}(u), i)\stackrel{s_i}{\to}w\),可以证明这个节点 \(w\) 一定存在。然后添加转移边 \(u\stackrel{s_i}{\to}v\),并且记录 \(\operatorname{len}(v)=\operatorname{len}(u)+2\) 即可。注意这里加 \(2\),因为在前后各添加了一个字符。
时间复杂度证明
可以通过数学归纳法证明字符串 \(s\) 的本质不同回文子串个数不超过 \(|s|\),因此 PAM 的状态数为 \(\mathcal O(|s|)\),同时显然转移数也为 \(\mathcal O(|s|)\)。
构建的剩余部分只有跳后缀链接的总复杂度不显然为 \(\mathcal O(|s|)\) 的。考虑跳后缀链接的过程,每跳一次使得状态在 \(\operatorname{fail}\) 树上的深度减少一,而添加新状态时最多使状态在 \(\operatorname{fail}\) 树上的深度增加一,因此最多跳 \(2|s|\) 次后缀链接。
综上,PAM 的时间复杂度为 \(\mathcal O(n)\)。
应用
本质不同回文子串个数
显然,答案即为 PAM 的状态数减 \(2\)(去掉奇根和偶根)。
在线求每个位置结尾的回文子串个数 | P5496 【模板】回文自动机(PAM)
一个位置结尾的回文子串个数就是该位置结尾的最长回文子串对应的状态在 \(\operatorname{fail}\) 树上的深度,构建 PAM 时维护一下即可。
代码
// Problem: P5496 【模板】回文自动机(PAM) // Contest: Luogu // URL: https://www.luogu.com.cn/problem/P5496 // Memory Limit: 256 MB // Time Limit: 500 ms // // Powered by CP Editor (https://cpeditor.org) //By: OIer rui_er #include <bits/stdc++.h> #define rep(x,y,z) for(int x=(y);x<=(z);x++) #define per(x,y,z) for(int x=(y);x>=(z);x--) #define debug(format...) fprintf(stderr, format) #define fileIO(s) do{freopen(s".in","r",stdin);freopen(s".out","w",stdout);}while(false) using namespace std; typedef long long ll; const int N = 5e5+5; int n, lst; char t[N]; template<typename T> void chkmin(T& x, T y) {if(x > y) x = y;} template<typename T> void chkmax(T& x, T y) {if(x < y) x = y;} struct State { int len, fail, nxt[26]; int dis; // }; struct PAM { State st[N+5]; char s[N]; int sz, lst; void init() { st[0].len = 0; st[0].fail = 1; st[1].len = -1; st[1].fail = 0; sz = 1; } int jump(int u, int i) { while(i - st[u].len - 1 < 1 || s[i-st[u].len-1] != s[i]) u = st[u].fail; return u; } void extend(int i) { int u = jump(lst, i), c = s[i] - 'a'; if(!st[u].nxt[c]) { st[++sz].fail = st[jump(st[u].fail, i)].nxt[c]; st[u].nxt[c] = sz; st[sz].len = st[u].len + 2; st[sz].dis = st[st[sz].fail].dis + 1; // } u = st[u].nxt[c]; lst = u; } }pam; int main() { scanf("%s", t+1); n = strlen(t+1); pam.init(); rep(i, 1, n) { if(i == 1) pam.s[i] = t[i]; else pam.s[i] = (t[i] + lst - 97) % 26 + 97; pam.extend(i); printf("%d%c", lst=pam.st[pam.lst].dis, " \n"[i==n]); } return 0; }
求每个回文子串的出现次数 | P3649 [APIO2014] 回文串
建出回文树,然后用类似 SAM 统计子串出现次数的方法倒着 DP 一遍。
注意每次访问到一个节点就要把 \(cnt\) 加一,否则会 WA 第七个点。
代码
// Problem: P3649 [APIO2014] 回文串 // Contest: Luogu // URL: https://www.luogu.com.cn/problem/P3649 // Memory Limit: 125 MB // Time Limit: 1000 ms // // Powered by CP Editor (https://cpeditor.org) //By: OIer rui_er #include <bits/stdc++.h> #define rep(x,y,z) for(int x=(y);x<=(z);x++) #define per(x,y,z) for(int x=(y);x>=(z);x--) #define debug(format...) fprintf(stderr, format) #define fileIO(s) do{freopen(s".in","r",stdin);freopen(s".out","w",stdout);}while(false) using namespace std; typedef long long ll; const int N = 3e5+5; int n; ll ans; template<typename T> void chkmin(T& x, T y) {if(x > y) x = y;} template<typename T> void chkmax(T& x, T y) {if(x < y) x = y;} struct State { int len, fail, nxt[26]; int cnt; }; struct PAM { State st[N+5]; char s[N]; int sz, lst; void init() { st[0].len = 0; st[0].fail = 1; st[1].len = -1; st[1].fail = 0; sz = 1; } int jump(int u, int i) { while(i - st[u].len - 1 < 1 || s[i-st[u].len-1] != s[i]) u = st[u].fail; return u; } void extend(int i) { int u = jump(lst, i), c = s[i] - 'a'; if(!st[u].nxt[c]) { st[++sz].fail = st[jump(st[u].fail, i)].nxt[c]; st[u].nxt[c] = sz; st[sz].len = st[u].len + 2; } u = st[u].nxt[c]; ++st[u].cnt; lst = u; } }pam; int main() { scanf("%s", pam.s+1); n = strlen(pam.s+1); pam.init(); rep(i, 1, n) pam.extend(i); per(i, pam.sz, 1) { pam.st[pam.st[i].fail].cnt += pam.st[i].cnt; chkmax(ans, 1LL*pam.st[i].cnt*pam.st[i].len); } printf("%lld\n", ans); return 0; }
这篇关于回文自动机(回文树)学习笔记的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-22[开源]10.3K+ Star!轻量强大的开源运维平台,超赞!
- 2024-11-21Flutter基础教程:新手入门指南
- 2024-11-21Flutter跨平台教程:新手入门详解
- 2024-11-21Flutter跨平台教程:新手入门与实践指南
- 2024-11-21Flutter列表组件教程:初学者指南
- 2024-11-21Flutter列表组件教程:新手入门指南
- 2024-11-21Flutter入门教程:初学者必看指南
- 2024-11-21Flutter入门教程:从零开始的Flutter开发指南
- 2024-11-21Flutter升级教程:新手必读的升级指南
- 2024-11-21Flutter升级教程:轻松掌握Flutter版本更新