题目:
考虑一个只包含小写拉丁字母的字符串s。我们定义s的一个子串t的“出现值”为t在s中的出现次数乘以t的长度。请你求出s的所有回文子串中的最大出现值。
分析:
回文树裸体。借此学习回文树,记录模板。
两个基本原理
- 对于任何一个串S,它的本质不同的回文串的个数不会超过|S|个
- 如果在串S后面加入一个字符,新增的本质不同的回文串的个数不会超过1个。
证明
利用数学归纳法来证明
当 |S|=1 | S | = 1 时,显然成立
如果我们假设
|S|=x−1
|
S
|
=
x
−
1
时成立,现在插入
x
x
位置,字符为
如果以xx位置结尾出现了两个新的本质不同的回文串
假设较长的从
l1
l
1
开始,较短的从
l2
l
2
开始
因为
|Sl1..r|>|Sl2..r|
|
S
l
1
.
.
r
|
>
|
S
l
2
.
.
r
|
又根据回文串对称的性质
所以
Sl2..r
S
l
2
.
.
r
在
Sl1..l1+r−l2
S
l
1
.
.
l
1
+
r
−
l
2
必定出现过
所以不存在两个本质不同的回文串
所以最多新增一个本质不同的回文串
所以到
x
x
位置出现的本质不同的回文串的个数最多为个
同时,我们也证明了每次插入一个新的字符,最多增加一个本质不同的回文子串。
回文树有以下几个易错点:
- 正因为每次增加一个字符最多增加一个新回文串,所以从增加上一个字符后的结点开始匹配,即每次从last开始。
- 新加入一个结点p时,要先给fail[p]赋值,再连接父亲的边。若先连父亲的边,那么fail就有可能连向自己。比如从空树开始,加入一个a,若先连父亲边,则fail[2] = 2
- 对于加入的单字符回文串,比如从空树开始加入了一个a,这时fail[2]应该是0,而不是1.若此时又来了一个a,就会发现fail[2] = 0才能形成aa。
- 找一个结点的fail时,可能会对
fail[v] = ch[k][c]
有疑问,ch[k][c]一定存在吗?我们知道,不存在时,比如这是一个第一次出现的字符,那么fail会指向0。其他情况,因为这个新结点代表的字符串是回文的,那么既然能在后缀上找到,那么前缀里必然也有一个,而前缀的那个串一定已经生成结点了,所以一定存在。
代码:
#include <bits/stdc++.h>
using namespace std;
#define ms(a,b) memset(a,b,sizeof(a))
#define lson rt*2,l,(l+r)/2
#define rson rt*2+1,(l+r)/2+1,r
typedef unsigned long long ull;
typedef long long ll;
const int MAXN=3e5+5;
const double EPS=1e-8;
const int INF=0x3f3f3f3f;
const int MOD = 1e9+7;
struct palin_tree{
int ch[MAXN][30], fail[MAXN], len[MAXN], last, tot, cnt[MAXN];
palin_tree(){
len[1] = -1; tot = 1; fail[0] = 1;
}
void insert(int c, int n, char *s){
int x = last;
while(s[n-1-len[x]] != s[n]) x = fail[x];
if(!ch[x][c]){
int v = ++tot, k = fail[x];
len[v] = len[x] + 2;
while(s[n-1-len[k]] != s[n]) k = fail[k];
fail[v] = ch[k][c];
ch[x][c] = v;
}
last = ch[x][c];
cnt[last]++;
}
void solve(){
ll ans = 0;
for(int i=tot;i;i--){
cnt[fail[i]] += cnt[i];
ans = max(ans, (ll)cnt[i] * len[i]);
}
cout << ans << endl;
}
}T;
char s[MAXN],n;
int main(){
ios::sync_with_stdio(false);
scanf("%s",s+1);
s[0] = 0;
int n = strlen(s+1);
for(int i=1;i<=n;i++){
T.insert(s[i]-'a',i,s);
}
T.solve();
return 0;
}