概述
一、适用问题
算法主要解决的是给出一个字符串, 复杂度下求出以字符串中任意一个节点为中心所能扩展的最大距离。
二、算法解析
扩充字符串
为了统一奇偶字符串,算法首先在每两个字符(包括头尾)之间加没出现的字符(如*),这样所有字符串长度就都是奇数了,简化了问题。
具体 过程
记录 表示 能向两边推(包括 )的最大距离,如果能求出 数组,那每一个回文串就都确定了。
我们假设 已经求好了,现在要求 。假设之前能达到的最右边为 ,对应的中点为 , 是 的对称点。
- 第一种情况,,即 。
- 第二种情况,,先设 ,然后再继续增加 ,并令 ,更新 。
由于 一定是递增的,因此时间复杂度为 ,可以发现一个串本质不同的回文串最多有 个,因此只有 增加的时候才会产生本质不同的回文串。
算法特点
- 充分利用了回文的性质,从而达到线性时间。
- 右端点递增过程中产生了所有的本质不同回文串,即一个串中本质不同回文串最多只有 个。
- 算法的核心在于求出每一个节点往两边推的最远距离,所有涉及该算法的问题也都是在这个功能上做文章。
三、 模板
void Manacher(char s1[]){
int len, tot, R = 0, pos = 0;
//对字符串加'#'号
len = strlen(s1+1);
s2[0] = '$';
s2[ln=1]='#';
for(int i = 1; i <= len; i++)
s2[++ln] = s1[i], s2[++ln] = '#';
//求p[i]数组
for(int i = 1; i <= ln; i++){
if(R >= i) p[i] = min(p[pos*2-i],R-i+1);
if(p[i] + i > R){
for(; s2[R+1] == s2[i*2-R-1] && R+1 <= ln && i*2-R-1 <= ln; R++); //小心多组数据
pos = i;
p[i] = R-i+1;
}
}
}
回文自动机概述
一、适用问题
回文自动机其实可以当作回文串问题的大杀器,主要可以解决以下问题:
- 本质不同回文串个数
- 每个本质不同回文串出现的次数
- 以某一个节点为结尾的回文串个数
- …
而且都是在 O(n) 的复杂度内解决了这些问题。但是要注意,回文自动机并不能完全替代马拉车算法,因为马拉车针对的是以一个节点为中心所能扩展的最远的距离,而回文自动机更多的是处理以一个节点为结尾的回文串数量。
二、算法解析
基础思想
与自动机一样,回文自动机的构造也使⽤了树的结构。
不同点在于回文自动机有两个根,一个是偶数⻓度回⽂串的根,一个是奇数⻓度回⽂串的根,简称为奇根和偶根。自动机中每个节点表⽰⼀个回⽂串,且都有⼀个 指针,指向当前节点所表⽰的回文串的最长回文后缀(不包括其本身)。
具体构建过程
首先将偶根的 指针指向奇根,奇根的 指针指向偶根,然后令奇根的长度为 ,偶根长度为 。
在构建过程中,始终要记录一个 指针,表示上一次插入之后所位于的回文自动机节点。然后判断插入当前节点之后是否能够形成一个新的回文串,如果不能则跳 指针,整体逻辑与自动机的构建没有太大差别。
指针表示当前节点所代表的回文串的最长后缀回文串,此处指针的定义与自动机有一定区别,但本质目的相同,都是为了尽可能地保存之前的匹配结果。
而 指针的构建是根据当前节点的父节点的 节点构建而来,与自动机的 构建没有太大差别。
大致上就是这样一个构建过程,具体细节可以查看下述模板,也可以查看 的图解构建。
统计每个本质不同回文串的出现次数
与自动机一样,每个节点所代表字符串出现的次数要加上其所有 子孙出现的次数,因此需要倒序遍历所有节点将节点出现次数累加到其 节点上。
三、回文自动机模板
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define per(i,a,b) for(int i = a; i >= b; i--)
const int N = 1e5+10;
using namespace std;
struct PAM{
#define KIND 26
int n,last,tot;
int len[N],trie[N][KIND],fail[N],cnt[N],S[N],num[N];
//len[i]: 节点i所代表的回文串长度, fail[i]: 当前回文串的最长回文后缀(不包括自身)
//cnt[i]: 节点i所代表的回文串的个数, S[i]: 第i次添加的字符, num[i]: 以第i个字符为结尾的回文串个数
//last: 上一个字符构成最长回文串的位置,方便下一个字符的插入
//tot: 总结点个数 = 本质不同的回文串的个数+2, n: 插入字符的个数
int newnode(int l){
rep(i,0,KIND-1) trie[tot][i] = 0;
cnt[tot] = 0, len[tot] = l, num[tot] = 0;
return tot++;
}
inline void init(){
tot = n = last = 0, newnode(0), newnode(-1);
S[0] = -1, fail[0] = 1;
}
int get_fail(int x){ //获取fail指针
while(S[n-len[x]-1] != S[n]) x = fail[x];
return x;
}
inline int insert(int c){ //插入字符
c -= 'a';
S[++n] = c;
int cur = get_fail(last);
//在节点cur前的字符与当前字符相同,即构成一个回文串
if(!trie[cur][c]){ //这个回文串没有出现过
int now = newnode(len[cur]+2);
fail[now] = trie[get_fail(fail[cur])][c];
trie[cur][c] = now;
num[now] = num[fail[now]]+1; //更新以当前字符为结尾的回文串的个数
}
last = trie[cur][c];
cnt[last]++; //更新当前回文串的个数
return num[last]; //返回以当前字符结尾的回文串的个数
}
void count(){ //统计每个本质不同回文串的个数
per(i,tot-1,0) cnt[fail[i]] += cnt[i];
}
}pam;
回文串系列习题
1. P1659 [国家集训队] 拉拉队排练
题意:
个字符,找出其中所有的奇长度回文串,按长度降序排列,求出前 个回文串长度的乘积。
思路:
模板题。直接对于所有本质不同回文串求出其个数,然后用 存到 中,再利用快速幂计算答案即可。
代码:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define per(i,a,b) for(int i = a; i >= b; i--)
typedef long long ll;
const int N = 1e6+1000;
const ll mod = 19930726;
using namespace std;
struct PAM{
#define KIND 26
int n,last,tot;
int len[N],trie[N][KIND],fail[N],S[N],num[N];
ll cnt[N];
//len[i]: 节点i所代表的回文串长度, fail[i]: 当前回文串的最长回文后缀(不包括自身)
//cnt[i]: 节点i所代表的回文串的个数, S[i]: 第i次添加的字符, num[i]: 以第i个字符为结尾的回文串个数
//last: 上一个字符构成最长回文串的位置,方便下一个字符的插入
//tot: 总结点个数 = 本质不同的回文串的个数+2, n: 插入字符的个数
int newnode(int l){
rep(i,0,KIND-1) trie[tot][i] = 0;
cnt[tot] = 0, len[tot] = l, num[tot] = 0;
return tot++;
}
inline void init(){
tot = n = last = 0, newnode(0), newnode(-1);
S[0] = -1, fail[0] = 1;
}
int get_fail(int x){ //获取fail指针
while(S[n-len[x]-1] != S[n]) x = fail[x];
return x;
}
inline int insert(int c){ //插入字符
c -= 'a';
S[++n] = c;
int cur = get_fail(last);
//在节点cur前的字符与当前字符相同,即构成一个回文串
if(!trie[cur][c]){ //这个回文串没有出现过
int now = newnode(len[cur]+2);
fail[now] = trie[get_fail(fail[cur])][c];
trie[cur][c] = now;
num[now] = num[fail[now]]+1; //更新以当前字符为结尾的回文串的个数
}
last = trie[cur][c];
cnt[last]++; //更新当前回文串的个数
return num[last]; //返回以当前字符结尾的回文串的个数
}
void count(){ //统计每个本质不同回文串的个数
per(i,tot-1,0) cnt[fail[i]] += cnt[i];
}
}pam;
int n;
ll k;
char buf[N];
vector<pair<int,ll> > v;
ll pow_mod(ll a,ll b){
ll ans = 1, base = a;
while(b){
if(b&1) ans = (ans*base)%mod;
base = (base*base)%mod;
b /= 2ll;
}
return ans;
}
int main(){
scanf("%d%lld",&n,&k);
scanf("%s",buf);
pam.init();
rep(i,0,n-1) pam.insert(buf[i]);
pam.count();
ll num = 0;
rep(i,0,pam.tot-1)
if(pam.len[i] > 0 && pam.len[i]%2 == 1){
v.push_back(make_pair(pam.len[i],pam.cnt[i]));
num += pam.cnt[i];
}
sort(v.begin(),v.end());
if(num < k) printf("-1\n");
else{
ll ans = 1;
int pos = v.size()-1;
while(k > 0){
if(k >= v[pos].second){
ans = (ans*pow_mod(v[pos].first,v[pos].second))%mod;
k -= v[pos].second;
}
else{
ans = (ans*pow_mod(v[pos].first,k))%mod;
k = 0;
}
pos--;
}
printf("%lld\n",ans);
}
return 0;
}
2. P3649 [APIO2014] 回文串
题意:
给定一个长度为 的字符串 ,我们定义 的一个子串的存在值为这个子串在 中出现的次数乘以这个子串的长度,输出所有回文子串的最大存在值。
思路:
模板题。直接求出每一个子串的出现次数,然后对于所有节点统计答案即可。(模板题贴一份代码即可)
3. P5496 回文自动机(PAM)
题意:
给定一个长度为 的字符串,求出以每个节点为结尾的回文子串个数。
思路:
模板题。 的 函数中自带以该节点为结尾的回文子串数的求解。(模板题贴一份代码即可,主要目的就是练练手)
4. P4287 [SHOI2011] 双倍回文
题意:
给定一个长度为 的字符串,询问该字符串中长度最长的双倍回文串的长度。双倍回文串指 ,如 即是双倍回文串,而 则不是。
思路:
由于只需要求最长回文串,并不要求输出个数,因此我们直接在 的 过程中求出答案即可。
首先明确一点, 右端点递增过程中,涉及到了所有的本质不同回文串,因此我们在每次右端点递增时,令当前节点为双倍回文串的中心节点,然后查询左半部分是否也为一个回文串,如果是则更新答案。
变型:
当然此题也可以进行延伸,可以将查询内容改成有多少个双倍回文串。如果询问有多少个双倍回文串的话,我的想法是用回文自动机+ 算法进行解决。
首先 求出每一个节点为中心所能扩展的最远距离。然后在根据当前字符串建立回文自动机,并且在构建时对于每个节点记录构成该节点的末尾座标 ,之后再遍历回文自动机上的所有节点,利用 计算的 数组来验证该节点所代表的回文串是否是双倍回文串即可。
代码:
#include <bits/stdc++.h>
const int N = 5e5+100;
using namespace std;
int n,tot,p[2*N],R,pos,ans;
char s[N],s2[2*N];
void manacher(){
s2[0] = '$';
s2[tot = 1] = '#';
for(int i = 1; i <= n; i++)
s2[++tot] = s[i], s2[++tot] = '#';
for(int i = 1; i <= tot; i++){
if(R >= i) p[i] = min(p[pos*2-i],R-i+1);
if(p[i] + i > R){
for(; s2[R+1] == s2[i*2-R-1]; R++){
int y = (3*i-R-1)/2;
if(s2[i] == '#' && s2[R+1] == '#' && (R+1-i)%4 == 0 && p[y] >= i-y+1) ans = max(ans,2*(i-y));
}
pos = i;
p[i] = R-i+1;
}
}
printf("%d\n",ans);
}
int main(){
scanf("%d",&n); scanf("%s",s+1);
manacher();
}
5. iChandu
题意:
给定一个长度为 的字符串,先可以选择该字符串中的一个节点,将其替换成 符号,问替换之后该字符串中本质不同回文串个数。
思路:
一个比较明显的考虑方式就是枚举替换的位置,然后计算该位置改变后增加的回文串数量与减少的回文串数量。
首先我们考虑增加的回文串数量,显然就是以该节点为中心所形成的回文串总数,因此我们直接用马拉车的 数组进行求解即可。
然后再来考虑减少的回文串数量。我们对于回文自动机中所有本质不同回文串的末尾出现位置记录一个 和 ,表示最远出现的位置和最早出现的位置,记该回文串长度为 。则如果 ,则修改位置出现在 中时,本质不同回文串数量就会减 ,因此我们维护一个差分数组, 加 , 减 ,然后对于每个自动机上的节点都这样处理一遍即可。
最后再枚举替换点统计答案。
代码:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define per(i,a,b) for(int i = a; i >= b; i--)
const int N = 1e5+10;
using namespace std;
char buf[N],s2[2*N];
int sum[N],p[2*N];
void dbg() {cout << "\n";}
template<typename T, typename... A> void dbg(T a, A... x) {cout << a << ' '; dbg(x...);}
#define logs(x...) {cout << #x << " -> "; dbg(x);}
struct PAM{
#define KIND 26
int n,last,tot;
int len[N],trie[N][KIND],fail[N],cnt[N],S[N],num[N],mx[N],mi[N];
//len[i]: 节点i所代表的回文串长度, fail[i]: 当前回文串的最长回文后缀(不包括自身)
//cnt[i]: 节点i所代表的回文串的个数, S[i]: 第i次添加的字符, num[i]: 以第i个字符为结尾的回文串个数
//last: 上一个字符构成最长回文串的位置,方便下一个字符的插入
//tot: 总结点个数 = 本质不同的回文串的个数+2, n: 插入字符的个数
int newnode(int l){
rep(i,0,KIND-1) trie[tot][i] = 0;
cnt[tot] = 0, len[tot] = l, num[tot] = 0, mx[tot] = 0, mi[tot] = 2*1e5;
return tot++;
}
inline void init(){
tot = n = last = 0, newnode(0), newnode(-1);
S[0] = -1, fail[0] = 1;
}
int get_fail(int x){ //获取fail指针
while(S[n-len[x]-1] != S[n]) x = fail[x];
return x;
}
inline int insert(int c,int pos){ //插入字符
c -= 'a';
S[++n] = c;
int cur = get_fail(last);
//在节点cur前的字符与当前字符相同,即构成一个回文串
if(!trie[cur][c]){ //这个回文串没有出现过
int now = newnode(len[cur]+2);
fail[now] = trie[get_fail(fail[cur])][c];
trie[cur][c] = now;
num[now] = num[fail[now]]+1; //更新以当前字符为结尾的回文串的个数
}
last = trie[cur][c];
cnt[last]++; //更新当前回文串的个数
mx[last] = max(mx[last],pos);
mi[last] = min(mi[last],pos); //更新每个回文串的最早/晚出现位置
return num[last]; //返回以当前字符结尾的回文串的个数
}
void count(){ //统计每个本质不同回文串的个数
per(i,tot-1,2){
cnt[fail[i]] += cnt[i];
mx[fail[i]] = max(mx[fail[i]],mx[i]);
mi[fail[i]] = min(mi[fail[i]],mi[i]);
if(mi[i] >= mx[i]-len[i]+1){
sum[mx[i]-len[i]+1] += 1;
sum[mi[i]+1] += -1;
}
}
}
}pam;
void manacher(){
int len = strlen(buf+1), tot = 1, R = 0, pos = 0;
s2[0] = '$'; s2[tot = 1] = '#';
for(int i = 1; i <= len; i++)
s2[++tot] = buf[i], s2[++tot] = '#';
for(int i = 1; i <= tot; i++){
if(R >= i) p[i] = min(p[pos*2-i],R-i+1);
if(p[i] + i > R){
for(; s2[R+1] == s2[i*2-R-1] && R+1 <= tot && i*2-R-1 <= tot; R++);
pos = i;
p[i] = R-i+1;
}
}
}
int main(){
int _; scanf("%d",&_);
while(_--){
int ans = 0, ct = 0;
scanf("%s",buf+1);
pam.init();
int len = strlen(buf+1);
rep(i,0,len) sum[i] = 0;
rep(i,1,len) pam.insert(buf[i],i);
pam.count();
manacher();
rep(i,1,len){
sum[i] += sum[i-1];
int to = pam.tot-2-sum[i]+((p[i*2]-2)/2+1);
if(ans < pam.tot-2-sum[i]+((p[i*2]-2)/2+1)){
ans = pam.tot-2-sum[i]+((p[i*2]-2)/2+1);
ct = 1;
}
else if(ans == pam.tot-2-sum[i]+((p[i*2]-2)/2+1)) ct++;
}
printf("%d %d\n",ans,ct);
}
return 0;
}
6. The Problem to Slow Down You
题意:
给定两个字符串,询问满足条件的四元组个数, 满足条件当且仅当 ,且 为回文串。
思路:
建两颗回文自动机,然后从奇根和偶根分别遍历,同时遍历两棵树,如果到达相同位置则计算对答案的贡献,也算是比较裸的一道题目。
代码:
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define per(i,a,b) for(int i = a; i >= b; i--)
const int N = 4*1e5+10;
typedef long long ll;
using namespace std;
char buf1[N],buf2[N];
ll ans = 0;
struct PAM{
#define KIND 26
int n,last,tot;
int len[N],trie[N][KIND],fail[N],S[N],num[N];
ll cnt[N];
//len[i]: 节点i所代表的回文串长度, fail[i]: 当前回文串的最长回文后缀(不包括自身)
//cnt[i]: 节点i所代表的回文串的个数, S[i]: 第i次添加的字符, num[i]: 以第i个字符为结尾的回文串个数
//last: 上一个字符构成最长回文串的位置,方便下一个字符的插入
//tot: 总结点个数 = 本质不同的回文串的个数+2, n: 插入字符的个数
int newnode(int l){
rep(i,0,KIND-1) trie[tot][i] = 0;
cnt[tot] = 0, len[tot] = l, num[tot] = 0;
return tot++;
}
inline void init(){
tot = n = last = 0, newnode(0), newnode(-1);
S[0] = -1, fail[0] = 1;
}
int get_fail(int x){ //获取fail指针
while(S[n-len[x]-1] != S[n]) x = fail[x];
return x;
}
inline int insert(int c){ //插入字符
c -= 'a';
S[++n] = c;
int cur = get_fail(last);
//在节点cur前的字符与当前字符相同,即构成一个回文串
if(!trie[cur][c]){ //这个回文串没有出现过
int now = newnode(len[cur]+2);
fail[now] = trie[get_fail(fail[cur])][c];
trie[cur][c] = now;
num[now] = num[fail[now]]+1; //更新以当前字符为结尾的回文串的个数
}
last = trie[cur][c];
cnt[last]++; //更新当前回文串的个数
return num[last]; //返回以当前字符结尾的回文串的个数
}
void count(){ //统计每个本质不同回文串的个数
per(i,tot-1,2) cnt[fail[i]] += cnt[i];
}
}pam[2];
void dfs(int t1,int t2){
rep(i,0,KIND-1){
int y1 = pam[0].trie[t1][i], y2 = pam[1].trie[t2][i];
if(y1 && y2){
dfs(y1,y2);
ans = ans+pam[0].cnt[y1]*pam[1].cnt[y2];
}
}
}
int main(){
int _; scanf("%d",&_);
rep(Ca,1,_){
ans = 0;
pam[0].init(); pam[1].init();
scanf("%s",buf1); scanf("%s",buf2);
int len1 = strlen(buf1), len2 = strlen(buf2);
rep(i,0,len1-1) pam[0].insert(buf1[i]);
rep(i,0,len2-1) pam[1].insert(buf2[i]);
pam[0].count(); pam[1].count();
dfs(0,0); dfs(1,1);
printf("Case #%d: %lld\n",Ca,ans);
}
return 0;
}
7. Finding Palindromes
题意:
给出 个字符串,求解在其中 个拼接中,回文串的数量。
思路:
考虑马拉车+字典树解决该问题。
两个串拼在一起是回文串,即 与 拼接之后为回文串,主要有两种情况。
- ,则将 与 的反串进行匹配, 中剩下的未匹配部分为回文串。
- ,则将 与 的反串进行匹配, 中剩下的未匹配部分为回文串。
分类完之后即可用马拉车算法求出回文串范围,然后用字典树进行反串匹配。
代码:
此题细节很多,非常容易写到一半进入暴躁模式…
还有就是我对拍了 组数据还没拍出错,但是就是过不了是为什么!暴躁!(现在过了…是空间开小了…脑子不清醒的作品…悄咪咪地放上代码…)
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <vector>
#define mem(a,b) memset(a,b,sizeof a);
#define rep(i,a,b) for(int i = a; i <= b; i++)
#define per(i,a,b) for(int i = a; i >= b; i--)
#define __ ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
typedef long long ll;
typedef double db;
const int N = 2e6+1000;
const db EPS = 1e-9;
using namespace std;
int n,trie[N][26],tot,p[2*N],flag[N],L[N],sum1[N],sum2[N];
char s[N],s1[N],s2[2*N];
ll ans = 0;
void init(char base[],int len,int op = 0){
//manacher
int ln = 1, R = 0, pos = 0;
rep(i,1,len) s1[i] = base[i-1];
s2[0] = '$'; s2[ln = 1] = '#';
for(int i = 1; i <= len; i++)
s2[++ln] = s1[i], s2[++ln] = '#';
for(int i = 1; i <= ln; i++){
if(R >= i) p[i] = min(p[pos*2-i],R-i+1);
if(p[i]+i > R){
for(; s2[R+1] == s2[i*2-R-1] && R+1 <= ln && i*2-R-1 <= ln; R++);
pos = i;
p[i] = R-i+1;
}
}
//预处理
if(op){
for(int i = 1; i <= len; i++){
int tp = i+(len-i)/2;
flag[i] = 0;
tp *= 2;
if((len-i) % 2 == 1) tp += 2;
else tp++;
if(p[tp] >= ln-tp+1) flag[i] = 1;
}
return;
}
for(int i = 1; i <= len; i++){
int tp = i-(i-1)/2;
flag[i] = 0;
tp *= 2;
if((i-1)%2 == 1) tp -= 2;
else tp--;
if(i != 1 && p[tp] >= tp) flag[i] = 1;
}
//插入字典树
int now = 0;
for(int i = len; i >= 1; i--){
if(!trie[now][s1[i]-'a']){
trie[now][s1[i]-'a'] = ++tot, sum1[tot] = sum2[tot] = 0;
rep(j,0,25) trie[tot][j] = 0;
}
now = trie[now][s1[i]-'a'];
if(flag[i]) sum2[now]++; //后半部分回文
if(i == 1) sum1[now]++; //末尾
}
}
void solve(char base[],int len){
init(base,len,1);
rep(i,1,len) s1[i] = base[i-1];
int now = 0;
for(int i = 1; i <= len; i++){
if(!trie[now][s1[i]-'a']) return;
now = trie[now][s1[i]-'a'];
if(i == len) ans += sum2[now]+sum1[now];
else if(flag[i]) ans += sum1[now];
}
}
int main()
{
while(~scanf("%d",&n)){
tot = 0; ans = 0;
rep(i,0,25) trie[0][i] = 0;
int l = 0;
rep(i,1,n){
int tp; scanf("%d%s",&tp,s+l);
init(s+l,tp);
L[i] = l; L[i+1] = l+tp;
l += tp;
}
ans = 0;
rep(i,1,n) solve(s+L[i],L[i+1]-L[i]);
printf("%lld\n",ans);
}
return 0;
}
后记
的旅行充满荆棘但一擡头便能看见无数束光,继续坚持,努力前行!???
本篇博客到这里就结束了,祝大家 愉快,一起爱上回文串把!(๑•̀ㅂ•́)و✧