P3649 [APIO2014] 回文串
题目链接
P3649 [APIO2014] 回文串
[APIO2014] 回文串
题目描述
给你一个由小写拉丁字母组成的字符串 \(s\)。我们定义 \(s\) 的一个子串的存在值为这个子串在 \(s\) 中出现的次数乘以这个子串的长度。
对于给你的这个字符串 \(s\),求所有回文子串中的最大存在值。
输入格式
一行,一个由小写拉丁字母(a~z)组成的非空字符串 \(s\)。
输出格式
输出一个整数,表示所有回文子串中的最大存在值。
样例 #1
样例输入 #1
abacaba
样例输出 #1
7
样例 #2
样例输入 #2
www
样例输出 #2
4
提示
【样例解释1】
用 \(\lvert s \rvert\) 表示字符串 \(s\) 的长度。
一个字符串 \(s_1 s_2 \dots s_{\lvert s \rvert}\) 的子串是一个非空字符串 \(s_i s_{i+1} \dots s_j\),其中 \(1 \leq i \leq j \leq \lvert s \rvert\)。每个字符串都是自己的子串。
一个字符串被称作回文串当且仅当这个字符串从左往右读和从右往左读都是相同的。
这个样例中,有 \(7\) 个回文子串 a,b,c,aba,aca,bacab,abacaba。他们的存在值分别为 \(4, 2, 1, 6, 3, 5, 7\)。
所以回文子串中最大的存在值为 \(7\)。
第一个子任务共 8 分,满足 \(1 \leq \lvert s \rvert \leq 100\)。
第二个子任务共 15 分,满足 \(1 \leq \lvert s \rvert \leq 1000\)。
第三个子任务共 24 分,满足 \(1 \leq \lvert s \rvert \leq 10000\)。
第四个子任务共 26 分,满足 \(1 \leq \lvert s \rvert \leq 100000\)。
第五个子任务共 27 分,满足 \(1 \leq \lvert s \rvert \leq 300000\)。
解题思路
回文树
回文树即回文自动机,类似于后缀自动机,具有转移边和后缀链,转移边按字符左右扩展,即在当前回文子串的基础上向左右各扩展一个一样的字符,后缀链即为当前回文子串的不包括本身的最长后缀回文子串,注意,回文串分为奇数和偶数,故需要建立两个根,奇根通过转移边连向所有长度为奇数的回文子串表示的状态节点,偶根通过转移边连向所有长度为偶数的回文子串表示的状态节点,一开始,偶根长度为 \(0\),奇根长度为 \(-1\),\(\color{red}{为什么这样设定?}\)因为长度每次通过转移边转移时其长度都要增加 \(2\),另外偶根的后缀链要连向奇根,\(\color{red}{为什么?}\)不妨这样理解:一个偶数的回文子串的长度的后缀链连向的节点表示的回文子串的长度至少要减少 \(1\),而偶根表示的是一个空串,其后缀链至少应该连向长度为 \(-1\) 的回文子串表示的节点,即奇根。这样除了奇根,所有的节点有且仅有一条后缀链,即回文自动机本身也是由转移边形成的 DAG 和后缀链形成的树组成。\(\color{red}{具体该如何构造?}\)类似于后缀自动机,采取增量构造的方式,假设当前已经构造好了 \(p-1\) 个字符的回文自动机,现在向回文自动机增加一个 \(s[p]\) 的字符,从上一个字符结尾的最长回文子串的节点开始,不断沿着后缀链走,直到 \(s[p]=s[p-len-1]\),即在此回文子串的前面有一个字符 \(s[p]\),这样在该字符左右添加 \(s[p]\) 这个字符即为 \(p\) 这个位置结尾的最长回文子串,\(\color{red}{该子串表示的状态节点的后缀链该如何指向?}\)如下:
此时找到了 A
这个结束状态的子串,如果 XAX
不存在回文自动机中的话再建立该字串表示的状态节点,现在的问题在于该状态节点的后缀链该如何指向,即对于 A
这个状态节点来说,由于后缀链指向的节点不能指向自己,即开始 A
应该先走向其后缀链表示的节点,然后再沿着后缀链走,直到该状态节点表示的回文子串的前面一个字符为 X
,此时在该回文子串上通过转移边 X
即得 XBX
,即为 XAX
这个回文子串表示的状态节点得后缀链指向的状态节点
另外,需要注意的一点,对于一个字符串 \(s\) 而言,其本质不同的回文子串最多只有 \(|s|\) 个,证明略
本题要求某个回文子串的出现次数乘以其长度的最大值,主要难点在于统计回文子串的出现次数上,类似于后缀自动机,即由于 DAG 本身就是一个拓扑图,因为通过反向转移边,长度长的回文子串一定包含长度短的回文子串,故从后往前递推统计即可
- 时间复杂度:\(O(n)\)
代码
// Problem: P3649 [APIO2014] 回文串
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3649
// Memory Limit: 125 MB
// Time Limit: 1000 ms
//
// Powered by CP Editor (https://cpeditor.org)
// %%%Skyqwq
#include <bits/stdc++.h>
//#define int long long
#define help {cin.tie(NULL); cout.tie(NULL);}
#define pb push_back
#define fi first
#define se second
#define mkp make_pair
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
typedef pair<LL, LL> PLL;
template <typename T> bool chkMax(T &x, T y) { return (y > x) ? x = y, 1 : 0; }
template <typename T> bool chkMin(T &x, T y) { return (y < x) ? x = y, 1 : 0; }
template <typename T> void inline read(T &x) {
int f = 1; x = 0; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -1; s = getchar(); }
while (s <= '9' && s >= '0') x = x * 10 + (s ^ 48), s = getchar();
x *= f;
}
const int N=3e5+5;
char s[N];
namespace pam
{
int sz,tot,lst;
int cnt[N],ch[N][26],len[N],fail[N];
char s[N];
int node(int l)
{
sz++;
memset(ch[sz],0,sizeof ch[sz]);
len[sz]=l;
fail[sz]=cnt[sz]=0;
return sz;
}
void clear()
{
sz=-1;
lst=0;
s[tot=0]='$';
node(0);
node(-1);
fail[0]=1;
}
int getfail(int x)
{
while(s[tot-len[x]-1]!=s[tot])x=fail[x];
return x;
}
void insert(char c)
{
s[++tot]=c;
int now=getfail(lst);
if(!ch[now][c-'a'])
{
int x=node(len[now]+2);
fail[x]=ch[getfail(fail[now])][c-'a'];
ch[now][c-'a']=x;
}
lst=ch[now][c-'a'];
cnt[lst]++;
}
LL solve()
{
LL res=0;
for(int i=sz;i>=0;i--)cnt[fail[i]]+=cnt[i];
for(int i=1;i<=sz;i++)res=max(res,(LL)len[i]*cnt[i]);
return res;
}
}
int main()
{
pam::clear();
scanf("%s",s+1);
for(int i=1;s[i];i++)pam::insert(s[i]);
printf("%lld",pam::solve());
return 0;
}