【模板】回文字符机 PAM
【模板】回文自动机 PAM
回文自动机
定义
回文自动机(Palindrome Automaton)是处理回文问题的利器。类似后缀自动机,回文自动机有:
- 状态:每个回文子串是一个状态,没有两个状态代表相同的回文子串。
- 转移:从一个状态出发有转移边,字符 \(r\) 的转移边表示在两边加上 \(r\) 所对应的回文子串,或者没有。
- Fail 指针:Fail 指针指向这个回文子串的一个后缀,满足它也是回文子串。
引理
对于一个回文串 \(s\),它有一段后缀 \(t\),满足:
- 如果 \(t\) 是 border,那么 \(t\) 是回文串(正着读和反着读确实一样)
- 如果 \(t\) 是回文串,那么 \(t\) 是 border(因为正着读和反着读一样)
以下是非常形象的证明:
所以我们的 fail 指针实际上指的是最长 border。
构建
先说怎么构建再谈正确性:首先我们需要一个偶回文根 \(0\) 和奇回文根 \(1\)(都是空的),并使得 \(fail_0=1\)。记录 \(len_u\) 表示这个状态对应的回文子串的长度,\(len_1=-1\)。欲将插入字符 \(s_i\),要从上次插入的地方 \(last\) 开始,我们要找到:谁的转移边应该到 \(u\)?\(u\) 的 fail 指针指向谁?
- 我们沿着 fail 树往上跳,跳到第一个满足:\(s[i-len[p]-1]=s[i]\) 的 \(p\),那么这个 \(p\) 的转移边 \(ch_{p,r}\) 应该就是我们新的回文子串。
- 如果 \(ch_{p,r}\) 已存在,算法结束,返回 \(last=ch_{p,r}\);否则新建点 \(ch_{p,r}:=u\)。
- \(len_u=len_p+2\)(由 \(len\) 定义)
- \(fail_u\) 的求值需要再跳一次 fail,找到另一个满足那个条件的,\(fail_u\) 就是它的 \(r\) 的转移边。
- \(last=u\)。结束。
观察到这个算法为什么没有更新其它的转移边,原因很简单,因为已经有了。可以理解一下,\(p\) 前面有一个 \(r\),往上跳到一个 \(q\) 前面也有 \(r\) 的话,根据 \(p\) 的回文,\(q\) 的在左面的映射的后面有 \(r\),同时前面有 \(r\),那么说明 \(q\) 的 \(r\) 的转移边已经存在,这个回文串已经被创建。这同时证明了一个串的回文子串最多有 \(O(n)\) 个,上界在 aa...a
取得。
code
template<int N,int M> struct palindam{
int s[N+10],cnt,ch[N+10][M],fail[N+10],len[N+10],tot;
int trans[N+10];
palindam():cnt(0),tot(1){fail[0]=1,len[1]=-1;}
bool diff(int p){int i=cnt-len[p]-1; return i<0||s[i]!=s[cnt];}
int getfail(int p){while(diff(p)) p=fail[p]; return p;}
int expand(int p,int r){
s[++cnt]=r,p=getfail(p);
if(ch[p][r]) return ch[p][r];
int u=++tot; len[u]=len[p]+2;
fail[u]=ch[getfail(fail[p])][r];
return ch[p][r]=u;
}
};
template <int N, int M>
struct palindam {
int str[N + 10], cnt, ch[N + 10][M], fail[N + 10], len[N + 10], dep[N + 10], tot;
palindam() : cnt(0), tot(1) {
memset(ch[0], 0, sizeof ch[0]);
memset(ch[1], 0, sizeof ch[1]);
fail[0] = 1;
len[1] = -1; str[0] = -1;
}
int getfail(int p) {
while (cnt - len[p] - 1 < 1 || str[cnt - len[p] - 1] != str[cnt])
p = fail[p];
return p;
}
int expand(int p, int r) {
str[++cnt] = r;
p = getfail(p);
if (ch[p][r]) return ch[p][r];
int u = ++tot;
memset(ch[u], 0, sizeof ch[u]);
len[u] = len[p] + 2;
fail[u] = ch[getfail(fail[p])][r];
dep[u] = dep[fail[u]] + 1;
ch[p][r] = u;
return u;
}
};
// 插入从 lst = 1 或 lst = 0 开始都对
应用
双回文子串
形如 \(ww^Rww^R\) 的字符串是双回文子串,其中 \(w\) 是正串,\(w^R\) 是反串。这是一个回文串,回文部分是回文串,长度是 \(4\) 的倍数。
我们枚举每个回文子串,那么我们就要找到他的 fail 祖先中刚好等于它的长度一半的 border。当然可以树上倍增,但我们可以维护一个指针 \(trans_u\) 表示 \(u\) 的祖先中 \(\leq len_u/2\) 的一个 border,枚举的时候查验一下是不是等于即可。在新建点的时候维护 \(trans\),首先如果 \(len_u\leq 2\) 那么取它的父亲即可,否则从 \(trans_p\) 开始向上跳,设跳到 \(q\),则 \(len_q+2\leq len_u/2\) 时且合法时停下(1. 这相当于跳 \(fail_u\) 2. 常数次跳跃),然后取 \(trans_u=ch_{q,r}\) 即可。
if(len[u]<=2) trans[u]=fail[u];
else{
int q=trans[p];
while(diff(q)||(len[q]+2)<<1>len[u]) q=fail[q];
trans[u]=ch[q][r];
}
回文划分
见 https://oi-wiki.org/string/pam/
这里抛出一些结论:
- Weak periodicity lemma:若 \(p,q\) 都是字符串 \(s\) 的 period,\(p+q\leq |s|+1\),则 \(\gcd(p,q)\) 也是 period。
- 所以,一个串的所有 border 构成 \(O(\log n)\) 个等差数列。
要做回文划分,就是 \(dp_i=1+\min_{j}f_j\) 其中 \([j-1,i]\) 回文,那么这个回文就是每个点插入的时候返回的点和他们的 fail,根据刚才的结论优化 DP 即可。这部分,OI wiki 可参考,这部分内容的完善已加入 todo list
广义 PAM
其实没有改多少。但是注意,PAM 复杂度分析基于当前点在 fail 树上深度(每次加入字符深度至多 +1),所以有可能还不能对 Trie 上建 PAM。
点击查看代码
template <int N>
struct palindam {
unordered_map<int, int> ch[N + 10];
int fail[N + 10], len[N + 10], dif[N + 10], tot, trans[N + 10];
palindam() : tot(1) { fail[0] = 1, len[1] = -1; }
int getfail(int p, char* s, int rlen) {
while (len[p] + 1 >= rlen || s[0] != s[-len[p] - 1]) p = fail[p];
return p;
}
int expand(int p, int r, char* s, int rlen) {
p = getfail(p, s, rlen);
if (ch[p][r]) return ch[p][r];
int u = ++tot;
len[u] = len[p] + 2;
fail[u] = ch[getfail(fail[p], s, rlen)][r];
dif[u] = len[u] - len[fail[u]];
return ch[p][r] = u;
}
}
for (int i = 1; i <= n; i++) {
string s;
cin >> s;
for (int j = 0, lst = 1; j < (int)s.size(); j++)
lst = M.expand(lst, s[j] - 'a', s.data() + j, j + 1);
}
本文来自博客园,作者:caijianhong,转载请注明原文链接:https://www.cnblogs.com/caijianhong/p/template-pam.html