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

posted @ 2022-10-27 20:53  I_N_V  阅读(78)  评论(0编辑  收藏  举报