[Luogu P3649] [APIO2014]回文串(后缀自动机)(或回文自动机)
题面
给出一个长度为(n)的字符串,求它的所有回文子串的出现次数乘以长度的最大值
(1 leq n leq 3 imes 10^5)
分析
SAM做法:
方法来自2015年国家集训队论文。吐槽:为什么网上的大部分题解都是SAM+Manacher啊,明明一个SAM就可以很简洁解决。
我们对原串构造后缀自动机,求出每个节点right(endpos)集合中的最大值,即最后一次出现的结束位置,记为(maxpos).然后把反串放到自动机上跑,如果当前的匹配串([i,i+len-1])覆盖了当前节点的(maxpos),那么([i,maxpos])是一个满足条件的回文子串.另外为了求它的出现次数,还需要求出right集合的元素个数。这些都可以通过自底向上遍历parent树求出。
PAM做法:
先建出回文自动机,计算一下每个节点代表的串出现了多少次。然后在fail树上从深到浅累加出现次数。(因为若S出现,那么串S的子串也出现),相当于加上后缀的贡献。
因为PAM里的每个节点都代表回文串,直接扫一遍求最大值即可。
代码
SAM:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define maxc 26
#define maxn 600000
using namespace std;
typedef long long ll;
struct SAM{
#define len(x) (t[x].len)
#define link(x) (t[x].link)
struct node{
int ch[maxc];
int link;
int len;
int maxpos;//right集合中的最大值
int cnt;//该位置子串的出现次数,即right集合大小,在parent树上可求出。
}t[maxn+5];
const int root=1;
int ptr=1;
int last=root;
void extend(char ch,int pos){
int c=ch-'a';
int p=last,cur=++ptr;
len(cur)=len(p)+1;
t[cur].maxpos=pos;
t[cur].cnt=1;
//cur和cur在parent树上的祖先的right集合中都会多出元素pos
//打一个标记,等建完之后再向上合并
while(p&&t[p].ch[c]==0){
t[p].ch[c]=cur;
p=link(p);
}
if(p==0) link(cur)=root;
else{
int q=t[p].ch[c];
if(len(p)+1==len(q)) link(cur)=q;
else{
int clo=++ptr;
t[clo]=t[q];
t[clo].cnt=0;
//复制clo的时候,一定要把clo的和right集合有关的标记清空
//因为复制clo的原因是len(p)+1<len(q),即状态q代表的串中,长度不超过len(p)+1的串会在当前串结尾出现,right集合会多出元素pos
//但是长度超过len(p)+1的串就不会被cur状态的right集合影响,所以拆完点记得清零
link(q)=link(cur)=clo;
len(clo)=len(p)+1;
while(p&&t[p].ch[c]==q){
t[p].ch[c]=clo;
p=t[p].link;
}
}
}
last=cur;
}
void insert(char *s){
int len=strlen(s+1);
for(int i=1;i<=len;i++) extend(s[i],i);
}
void topo_sort(){
queue<int>q;
static int in[maxn+5];
for(int i=1;i<=ptr;i++) in[link(i)]++;//实际上是拓扑序的逆序
for(int i=1;i<=ptr;i++) if(!in[i]) q.push(i);
while(!q.empty()){
int x=q.front();
q.pop();
if(link(x)==0) continue;
t[link(x)].maxpos=max(t[link(x)].maxpos,t[x].maxpos);
t[link(x)].cnt+=t[x].cnt;
in[link(x)]--;
if(!in[link(x)]) q.push(link(x));
}
}
ll solve(char *s){
static bool vis[maxn+5];
ll ans=0;
int len=strlen(s+1);
int matl=0;//匹配长度
int x=root;
insert(s);
topo_sort();
for(int i=len;i>=1;i--){
int c=s[i]-'a';
while(x&&!t[x].ch[c]){
x=link(x);
matl=len(x);
}
if(t[x].ch[c]){
matl++;
x=t[x].ch[c];
}
int tmp=matl;
for(int y=x;y!=root&&!vis[y];y=link(y),tmp=len(y)){
if(y!=x) vis[y]=1;
if(t[y].maxpos>=i&&t[y].maxpos<=i+tmp-1) ans=max(ans,1ll*(t[y].maxpos-i+1)*t[y].cnt);
//如果反串的匹配串[i,i+tmp-1]覆盖了maxpos,那[i,maxpos]就是一个回文串
}
}
return ans;
}
#undef len
#undef link
}T;
char str[maxn+5];
int main(){
scanf("%s",str+1);
printf("%lld
",T.solve(str));
}
PAM:
//https://www.luogu.com.cn/problem/P3649
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxc 26
#define maxn 300000
using namespace std;
typedef long long ll;
int n;
char s[maxn+5];
struct PAM{
struct node{
int fail;
int ch[maxc];
int cnt;//覆盖次数
int len;
}t[maxn+5];
int ptr;
int last;
void ini(){
ptr=1;
t[0].len=0;
t[0].fail=1;
t[1].len=-1;
t[1].fail=0;
last=0;
}
int get_fail(int x,int n){
while(s[n-t[x].len-1]!=s[n]) x=t[x].fail;
return x;
}
void insert(int c,int pos){
int p=get_fail(last,pos);
if(t[p].ch[c]==0){
int cur=++ptr;
t[cur].len=t[p].len+2;
t[cur].fail=t[get_fail(t[p].fail,pos)].ch[c];
t[p].ch[c]=cur;
}
t[t[p].ch[c]].cnt++;
last=t[p].ch[c];
}
inline ll calc(){
ll ans=0;
for(int i=ptr;i>=1;i--) t[t[i].fail].cnt+=t[i].cnt;//回文自动机的性质,似乎fail树中后代的编号一定更大
for(int i=1;i<=ptr;i++) ans=max(ans,1ll*t[i].cnt*t[i].len);
return ans;
}
}T;
int main(){
scanf("%s",s+1);
n=strlen(s+1);
T.ini();
for(int i=1;i<=n;i++) T.insert(s[i]-'a',i);
printf("%lld
",T.calc());
}