Manacher&PAM
前置知识
反转串 R(S)
一个字符串 S = S[1]S[2] · · · S[n]
其反串为R(S) = S[n]S[n − 1] · · · S[1]。
回文串性质
回文半径二分性
回文半径-1 等价于同时删掉回文串的首尾字母,依然是回文串。
回文串和Border
对于回文串 S,回文前(后)缀等价于Border
Manacher
模板
vector<int> manacher(string &a)
{
string b = "$|";
for (auto i : a)
{
b += i;
b += '|';
}
int len = b.length();
vector<int> hw(b.length()); //hw:最大延伸长长度
int maxright = 1, mid = 1;
for (int i = 1; i < len; i++)
{
if (i < maxright)
hw[i] = min(hw[mid * 2 - i], hw[mid] + mid - i);
else
hw[i] = 1;
while (b[i - hw[i]] == b[i + hw[i]])
hw[i]++;
if (i + hw[i] > maxright)
{
maxright = i + hw[i];
mid = i;
}
}
a = b;
return hw;
}
用法
求每个回文中心的回文半径
求本质不同回文串:
在 Manacher 中,新的回文串一定出现在使得最右串右移的时候。
因此本质不同回文串至多 n 个,把所有更新最右回文串去重即得到本质不同回文串。
例题
1.如果我让你查回文你还爱我吗
题意:给出一个字符串 S,和 Q 次询问,每次询问 S[L, R] 有多少个回文子串。
题解:
从询问入手,由于回文串天生的对称性,因此可以把问题分成左右两半来思考:
如果回文串的中心在询问区间的左半边,那么左端点将称为回文半径长度的唯一限制,中心在右半边同理。
利用 Manacher 预处理回文半径,之后离线处理,用树状数组来维护左/右半边的范围内的回文中心个数。
点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("Yes");return;}
#define NO {puts("No");return ;}
using namespace std;
const int maxn=7e5+101;
const int MOD=998244353;
const int inf=2147483647;
const double pi=acos(-1);
const double eps=1e-8;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
template <class T>
struct Fenwick {
const int n;
vector<T> tr1, tr2;
Fenwick(int n) : n(n), tr1(n + 1), tr2(n + 1) {}
int lowbit(int x){return x&(-x);}
void init(int n, vector<T>& v) {
for (int i = 1; i <= n; i++)
add(i, v[i] - v[i - 1]);
}
void add(int x, T c) {
for (int i = x; i <= n; i += lowbit(i))
tr1[i] += c, tr2[i] += c * x;
}
void range_add(int l, int r, T x) {
add(l, x), add(r + 1, -x);
}
T query(int p) {
T res = 0;
for (int i = p; i; i -= lowbit(i))
res += (p + 1) * tr1[i] - tr2[i];
return res;
}
T range_query(int l, int r) {
return query(r) - query(l - 1);
}
};
vector<int> manacher(string &a)
{
string b = "$|";
for (auto i : a)
{
b += i;
b += '|';
}
int len = b.length();
vector<int> hw(b.length()); //hw:最大延伸长长度
int maxright = 1, mid = 1;
for (int i = 1; i < len; i++)
{
if (i < maxright)
hw[i] = min(hw[mid * 2 - i], hw[mid] + mid - i);
else
hw[i] = 0;
while (b[i - hw[i]] == b[i + hw[i]])
hw[i]++;
if (i + hw[i] > maxright)
{
maxright = i + hw[i];
mid = i;
}
}
a = b;
return hw;
}
int n,q;
ll ans[maxn];
int main(){
n=read();q=read();
string s;cin>>s;
vector<int> a=manacher(s);
int m=a.size()-1;
vector<pa> ql[m+1],qr[m+1];
for(int i=1;i<=q;i++){
int l=read(),r=read();
ans[i]-=r-l+2;//把'|'单独成回文串删掉
l=l*2-1,r=r*2+1;
int mid=(l+r)>>1;
ql[mid].pb(mp(l,i));
qr[mid+1].pb(mp(r,i));
}
Fenwick<ll>tl(m+1);//左半边
for(int i=1;i<=m;i++){
tl.range_add(i-a[i]+1,i,1);
for(auto j:ql[i]){
ans[j.se]+=tl.range_query(j.fi,i);
}
}
Fenwick<ll>tr(m+1);
for(int i=m;i>=1;i--){
tr.range_add(i,i+a[i]-1,1);
for(auto j:qr[i]){
ans[j.se]+=tr.range_query(i,j.fi);
}
}
for(int i=1;i<=q;i++)cout<<(ans[i]>>1)<<endl;
//每次会多统计一倍,因为计算了'|'的位置
return 0;
}
2.[国家集训队]拉拉队排练
题意:求前 K 大的奇回文子串长度乘积。
题解:
只要奇数的回文串,那么用桶记录长度为i的回文串个数
大的奇数回文串-2就是新的回文串,用前缀和统计长度为i的回文串个数,用快速幂求答案
点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("Yes");return;}
#define NO {puts("No");return ;}
using namespace std;
const int maxn=7e5+101;
const int MOD=19930726;
const int inf=2147483647;
const double pi=acos(-1);
const double eps=1e-8;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
vector<int> manacher(string &a)
{
string b = "$|";
for (auto i : a)
{
b += i;
b += '|';
}
int len = b.length();
vector<int> hw(b.length()); //hw:最大延伸长长度
int maxright = 1, mid = 1;
for (int i = 1; i < len; i++)
{
if (i < maxright)
hw[i] = min(hw[mid * 2 - i], hw[mid] + mid - i);
else
hw[i] = 0;
while (b[i - hw[i]] == b[i + hw[i]])
hw[i]++;
if (i + hw[i] > maxright)
{
maxright = i + hw[i];
mid = i;
}
}
a = b;
return hw;
}
ll power(ll x,ll y){
ll ans=1;
while(y){
if(y&1)ans=ans*x%MOD;
y>>=1;x=x*x%MOD;
}
return ans;
}
int main(){
ll n=read(),k=read();
string s;cin>>s;
vector<int>a=manacher(s);
int m=a.size()-1;
vector<ll>sum(n+2);
for(int i=2;i<=m;i+=2){
int l=i-a[i]+1,r=i+a[i]-1,len=r-l;
len>>=1;sum[len]++;
}
ll ans=1,cnt=0;
for(int i=n;i>=1;i--){
if(i%2==0){sum[i]=sum[i+1];continue;}
sum[i]+=sum[i+1];sum[i]%=MOD;
ans=ans*power(i,min(k,sum[i]))%MOD;
k-=min(k,sum[i]);
}
ans=(ans%MOD+MOD)%MOD;
if(k!=0)puts("-1");
else cout<<ans<<endl;
return 0;
}
3.[POI2010]ANT-Antisymmetry
题意:给出一个 01 串 S,求最长的反对称子串。反对称串 T 定义为:将 R(T) 逐位取反之后再反转后等于原串 T,则 T 是一个反对称串,例如 010101。
题解:
也就是说我们可以定义一个新的回文串,首位两个字符串异或为1
在新的相等运算意义下进行 Manacher 即可
点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("Yes");return;}
#define NO {puts("No");return ;}
using namespace std;
const int maxn=7e5+101;
const int MOD=19930726;
const int inf=2147483647;
const double pi=acos(-1);
const double eps=1e-8;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
vector<ll> manacher(string &a)
{
string b = "$|";
for (auto i : a)
{
b += i;
b += '|';
}
map<char,char>mm;
mm['|']='|';
mm['1']='0';
mm['$']='$';
mm['0']='1';
int len = b.length();
vector<ll> hw(b.length()); //hw:最大延伸长长度
int maxright = 1, mid = 1;
for (int i = 1; i < len; i++)
{
if (i < maxright)
hw[i] = min(hw[mid * 2 - i], hw[mid] + mid - i);
while (mm[b[i - hw[i]]] == b[i + hw[i]])
hw[i]++;
if (i + hw[i] > maxright)
{
maxright = i + hw[i];
mid = i;
}
}
a = b;
return hw;
}
int main(){
int n=read();
string s;cin>>s;
vector<ll>a=manacher(s);
int m=a.size()-1;
ll ans=0;
for(int i=1;i<=m;i+=2)ans+=a[i]/2ll;
cout<<ans<<endl;
return 0;
}
4.[SHOI2011]双倍回文
题意:给出一个字符串 S,求最长的双倍回文子串。若一个串能表示成T R(T)T R(T) 的形式,则称为一个双倍回文串,例如 abbaabba 。
题解:
由于本质不同回文串只有 n 个,因此逐一检查是否是双倍回文即可。由于新的回文串一定发生于更新最右回文串的时候,所以在发生暴力拓展的时候,顺便检查即可。
点击查看代码
#include <bits/stdc++.h>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("Yes");return;}
#define NO {puts("No");return ;}
using namespace std;
const int maxn=7e5+101;
const int MOD=19930726;
const int inf=2147483647;
const double pi=acos(-1);
const double eps=1e-8;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
int ans;
vector<int> manacher(string &a)
{
string b = "$|";
for (auto i : a)
{
b += i;
b += '|';
}
int len = b.length();
vector<int> hw(b.length()); //hw:最大延伸长长度
int maxright = 1, mid = 1;
for (int i = 1; i < len; i++)
{
if (i < maxright)
hw[i] = min(hw[mid * 2 - i], hw[mid] + mid - i);
while (b[i - hw[i]] == b[i + hw[i]]){
hw[i]++;
//本质不同的回文串,会在hw[i]+i>maxright产生
if(i+hw[i]>maxright && i%2==1 && (hw[i]-1)%4==0 && (hw[i-hw[i]/2]>hw[i]/2))ans=max(ans,hw[i]-1);
}
if (i + hw[i] > maxright)
{
maxright = i + hw[i];
mid = i;
}
}
a = b;
return hw;
}
int main(){
int n=read();string s;cin>>s;
vector<int>a=manacher(s);
cout<<ans;
return 0;
}
PAM
Palindrome Automaton(回文自动机,回文树)是一种能够识别所有回文子串的数据结构,结构十分类似于之前讲的 AC自动机。
1 节点:节点数至多 N 个,每个节点代表了一种回文串。用 S(u) 表示节点 u 代表的回文串。len[u] = |S(u)|
2 后继边:每个后继边上有一个字母。用 trans(u, ch) = v 表示 u 节点有后继边 ch 指向 v 节点。则有 S(v) =ch S(u) ch,以及len[v] = len[u] + 2
3 失配边:每个节点都有一个失配边,用 fail[u] = v 表示 u 节点的失配边指向了 v 节点。则有 S(v) 是 S(u) 的最大 Border,即最长回文后缀。
复杂度分析
使用 AC 自动机完全相同的势能分析方法,即可得知时间复杂度为线性。
后继边至多 N 条,朴素数组存边空间复杂度为 |S| · |Σ|。Hash 表存边可以做到线性,和期望 O(1) 随机访问。
如果字符集太大,可以使用 Map 或者手写线段树存边
例题
1.[APIO2014]回文串
题意:给出一个字符串 S,定义 S 的子串 T 的价值为 F(T) = |T| · CNT(T),其中 CNT(T) 表示 T 在 S 中的出现次数。
求 S 价值最大的回文子串。
题解:
PAM 可以求出所有本质不同子串,且只有 N 个,那么本题实际上就是要统计每种回文串的出现次数。
可以在构建 PAM 的时候额外维护一个 cnt 数组,表示每个 PAM 节点对应多少个位置的最长回文后缀,也就是每个点有多少次成为了 Last 节点。
这样每个节点代表的回文串的出现次数等于,Fail 树子树的和。
卡常小技巧:节点编号从大到小就是这棵 Fail 树的拓扑排序(对于ac自动机,没有这个性质)。
点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdlib>
#include<cstdio>
#include<string>
#include<cmath>
#include<queue>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=5e6+11;
const int MOD=100000;
const int inf=2147483647-2;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
struct PAM{
int tot,ch[maxn][26],len[maxn],fail[maxn];
int last,all,cnt[maxn],text[maxn];
int newnode(int lenth){
memset(ch[tot],0,sizeof(int)*26);
cnt[tot]=0;len[tot]=lenth;
return tot++;
}
void init(){
last=tot=all=0;
newnode(0);newnode(-1);
text[0]=-1;fail[0]=1;
return ;
}
int getfail(int x){
while(text[all-len[x]-1]!=text[all])x=fail[x];
return x;
}
void extend(int c){
text[++all]=c;
int x=getfail(last);
if(!ch[x][c]){
int y=newnode(len[x]+2);
fail[y]=ch[getfail(fail[x])][c];
ch[x][c]=y;
}
cnt[last=ch[x][c]]++;
return ;
}
void add(string s){
int lenth=s.length();
for(int i=0;i<lenth;i++)extend(s[i]-'a');
return ;
}
void count(){
for(int i=tot-1;i>=0;i--)cnt[fail[i]]+=cnt[i];
return ;
}
}P;
int main(){
int n=read();string s;cin>>s;
P.init();P.add(s);P.count();
ll ans=0;
for(int i=2;i<P.tot;i++)ans=max(ans,1ll*P.len[i]*P.cnt[i]);
cout<<ans<<endl;
return 0;
}
2.Colorful String
题意:给出一个字符串 S,定义一个字符串的价值为:出现字母的种类数。求 S 所有回文子串的价值之和。
题解:
本题除了求每个本质不同子串的出现次数外,还需要求每个本质不同子串出现的字母种类数。
可以在 PAM 的每个节点额外维护一个 mask,表示这个点代表的回文串用到了哪些字母,在新建节点的时候顺便维护即可。
点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdlib>
#include<cstdio>
#include<string>
#include<cmath>
#include<queue>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define YES {puts("YES");return;}
#define NO {puts("NO");return ;}
using namespace std;
const int maxn=5e6+11;
const int MOD=100000;
const int inf=2147483647-2;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
struct PAM{
int tot,ch[maxn][26],len[maxn],fail[maxn];
int last,all,cnt[maxn],text[maxn];
int mask[maxn][26];//记录mask[i][j]记录i节点代表的回文串有没有j这个字符
int newnode(int lenth){
memset(ch[tot],0,sizeof(int)*26);
cnt[tot]=0;len[tot]=lenth;
return tot++;
}
void init(){
last=tot=all=0;
newnode(0);newnode(-1);
text[0]=-1;fail[0]=1;
return ;
}
int getfail(int x){
while(text[all-len[x]-1]!=text[all])x=fail[x];
return x;
}
void extend(int c){
text[++all]=c;
int x=getfail(last);
if(!ch[x][c]){
int y=newnode(len[x]+2);
fail[y]=ch[getfail(fail[x])][c];
ch[x][c]=y;
for(int j=0;j<26;j++){
if(mask[x][j])mask[y][j]=1;
}
mask[y][c]=1;
}
cnt[last=ch[x][c]]++;
return ;
}
void add(string s){
int lenth=s.length();
for(int i=0;i<lenth;i++)extend(s[i]-'a');
return ;
}
ll count(){
ll ans=0;
for(int i=tot-1;i>=0;i--){
cnt[fail[i]]+=cnt[i];
int now=0;
for(int j=0;j<26;j++)now+=mask[i][j];
ans+=1ll*cnt[i]*now;
}
return ans;
}
}P;
int main(){
string s;cin>>s;
P.init();P.add(s);
cout<<P.count();
return 0;
}
扩展知识
Palindrome Series
用于解决枚举回文后缀的 DP:
由于回文串的特殊性质:回文串的回文后缀一定是 Border。所以枚举回文后缀等价于枚举最大回文后缀的 Border,而 Border 具有良好的等差数列性质,PAM 的 Fail 链接就是 Border 链接。
因此:Palindrome Series= PAM+Border Series
Link