回文
\(reverse\) 之后和原来相同的字符串
比如 \(asdffdsa\) 和 \(asdfdsa\) 都是回文
判断一个串是否为回文可以扫一遍,可以使用 \(hash\)等等
常用的相关算法还有有 \(manacher\) 和回文自动机
manacher
核心思想在于维护最远的回文中心和回文半径,利用回文的对称性最大限度的利用已知信息,降低复杂度。
code
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn = 22000005;
char c[maxn], s[maxn];
int p[maxn];
int main(){
scanf("%s",c + 1);
int n = strlen(c + 1), cnt = 0;
s[++cnt] = '~', s[++cnt] = '#';
for(int i = 1; i <= n; ++i)s[++cnt] = c[i], s[++cnt] = '#';
s[++cnt] = '!';
int mr = 0, mid = 0, ans = 0;
for(int i = 2; i < cnt; ++i){
if(i <= mr)p[i] = min(p[mid * 2 - i], mr - i + 1);
else p[i] = 1;
while(s[i - p[i]] == s[i + p[i]]) ++p[i];
if(i + p[i] > mr)mr = i + p[i] - 1, mid = i;
ans = max(ans, p[i]);
}
printf("%d\n",ans - 1);
return 0;
}
回文自动机/回文树/PAM
回文自动机的核心思想类似于 \(AC\) 自动机,是匹配,失配这样的模式,但是你们好像还没学 \(AC\) 自动机。。。
不同于 \(manacher\) 将回文信息放在回文中心处, \(PAM\) 的信息在树上,或者你也可以说他在末尾,通过不断匹配尝试拓展。
他的\(fail\) 指针是指向最长回文后缀
奇偶回文的处理也不同于 \(manacher\)
状态数等具体的证明请看oiwiki
这里只放一下他的复杂度证明
关于 \(fail\)
这有一个一步步图示\(abbaabba\)的,如果还是不太懂可以看看
https://blog.csdn.net/Clove_unique/article/details/53750322
code
#include<bits/stdc++.h>
using namespace std;
const int maxn = 5e5 + 55;
char s[maxn];
int ch[maxn][26], len[maxn], fail[maxn], dep[maxn], cnt = 1;
int main(){
scanf("%s",s + 1);
s[0] = '#';
int n = strlen(s + 1);
int las = 0, ans = 0;
fail[0] = 1; len[1] = -1;
for(int i = 1; i <= n; ++i){
if(i != 1)s[i] = (s[i] - 97 + ans) % 26 + 97;
while(s[i - len[las] - 1] != s[i])las = fail[las];
if(!ch[las][s[i] - 'a']){
len[++cnt] = len[las] + 2;
int j = fail[las];
while(s[i - len[j] - 1] != s[i])j = fail[j];
fail[cnt] = ch[j][s[i] - 'a'];
ch[las][s[i] - 'a'] = cnt;
dep[cnt] = dep[fail[cnt]] + 1;
}
las = ch[las][s[i] - 'a'];
printf("%d ",ans = dep[las]);
}
return 0;
}
例题
简要题解
在匹配过程中把相等条件改为不等即可
简要题解(PAM)
考虑双倍回文本身也是回文,而他的\(W^RW\) 也是回文,也就是他在跳\(fail\)的过程中能跳到一个点,其长度为自己长度的一半且为偶数,在维护回文自动机的过程中多维护一个类似 \(fail\) 的指针,指向小于等于当前节点长度一半的最长回文后缀。
当恰好等于一半且为偶数时可以统计答案。
简要题解(马拉车)
考虑所有O(n)个本质不同的回文串(证明见 \(oi-wiki\)),对每个回文串都可以\(O(1)\)判断。具体实现中,只需要在\(manacher\)中\(mx\)更新时,判断所有新出现的回文串的前一半是否为回文串即可。
(洛谷第一篇题解)
简要题解
这个题确实没看到马拉车怎么做
首先观察性质
发现你能翻转的话一定会用翻转($x + 1 \leq x \times 2 $)
答案一定是一个回文再暴力拼接构成
那现在需要构成每种回文的最小操作次数
我们的操作过程大体是暴力加,翻倍,暴力加,翻倍。。。
翻倍?还是用上面那个\(trans\)指针
简要题解
使用 \(PAM\) 考虑如何维护每个回文的出现次数
一个回文出现,他的所有回文后缀一定出现,走到每个节点加一最后在 \(fail\) 树上做子树和即可
简要题解
\(PAM\) 比较方便,记录每个点向前和向后的最长回文,然后你就会做了
马拉车也是类似,你在回文中心记录了长度,顺推一遍倒推一遍,每推一个位置就减去\(2\)
- 萨鲁曼的半兽人
求最少将序列划分成几段,使得每一段都为回文,划分的段可以有交叉
简要题解
求回文可以hash + 二分,但是回文自动机显然更方便(而且回文长度在右端点)
用线段树维护DP数组,只需要在最长的回文范围内查询最小值 + 1更新当前位置答案
如果使用马拉车,也是类似的思路,但是要复杂一点,在左侧和前一个位置取 \(min\) 再加一,更新右侧区间