Problem
求长度为 \(n\) 的、字符集大小 \(5000\) 的串有多少个偶数长的子串前一半和后一半循环同构。
\(n \leq 5000\)
Solution
分析
问题即为求有多少个子区间可以表示成 \(uvvu\) (\(|u|\neq 0\),\(|v|\) 可以为 \(0\))的形式。
我们发现,如果以两个 \(v\) 作为中心,从中心分别向两边扩展,从这个过程可以得到两个字符串 \(a=v^Ru^R,b=vu\)(\(u^R\) 表示 \(u\) 的反转),那么 \(s=a_1b_1a_2b_2\dots a_nb_n\) 这个串相当于是 \(v\) 和 \(v^R\) 交错拼起来,\(u\) 和 \(u^R\) 交错拼起来,然后再把这两个得到的字符串连起来。那么 \(s\) 这个串相当于两个偶回文串拼接而成。
考虑枚举中心,然后得到向两边扩展的字符串 \(a,b\),两个字符串都只保留前缀 \(1\dots \min\{|a|,|b|\}\),然后根据上面的方式得到字符串 \(s\)。那么我们就是需要统计,\(s\) 有多少个前缀,能够表示成两个偶回文串拼接而成的结果(注意特判只有 \(|v|=0\))。
Lemma 1: 对于一个双回文串 \(s\)(能够表示成两个非空回文串拼接的结果),若 \(s=x_1x_2=y_1y_2=z_1z_2(|x_1|<|y_1|<|z_1|)\) 且 \(x_2,y_1,y_2,z_1\) 是回文串,则 \(x_1,z_2\) 也是回文串。
证明: 下面只证 \(x_1\) 是回文串,\(z_2\) 同理。
如上图,设 \(z_1=y_1v\),则 \(v\) 是 \(y_2\) 的前缀,\(v^R\) 是 \(x_2,y_2\) 的后缀,\(v\) 是 \(x_2\) 的前缀,于是 \(x_1v\) 是 \(z_1\) 的前缀。
\(y_1\) 是 \(z_1\) 的 border,所以 \(|v|\) 是 \(z_1\) 的 period,于是 \(|v|\) 也是 \(x_1v\) 的 period。
所以 \(x_1\) 是 \(v^{\infty}\) 的后缀。
\(v^R\) 是 \(x_1,z_1\) 的前缀,而 \(|v|\) 是 \(x_1\) 的 period,所以 \(x_1\) 是 \(\left(v^R\right)^{\infty}\) 的前缀。
故 \(x_1\) 是回文串。\(\square\)
Lemma 2: 对于一个双回文串 \(s\),存在一种回文划分 \(s=ab\)(\(a,b\) 均为回文串且非空),使得 \(a\) 是 \(s\) 的最长回文前缀,或 \(b\) 是 \(s\) 的最长回文后缀。
可以根据 Lemma 1 加上一些分类得到,这里不详细证明。
Lemma 3: 对于一个双偶回文串 \(s\)(能表示成两个偶回文串拼接的结果),存在一种回文划分 \(s=ab\)(\(a,b\) 均为偶回文串且非空),使得 \(a\) 是 \(s\) 的最长偶回文前缀,或 \(b\) 是 \(s\) 的最长偶回文后缀。
将 Lemma 1 和 Lemma 2 的「回文串」改成「偶回文串」结论仍然成立。
Manacher 做法
因此我们只需要求出所有前缀的最长偶回文前缀和最长偶回文后缀,并分别判断剩下的部分是否回文。
求所有前缀的最长偶回文后缀,可以从左往右扫,维护一个当前的回文串能延伸右边界 \(rit\)。每次枚举到新的回文中心 \(i\),只需要更新左端点在区间 \((rit,i+r_i)\) 内的信息即可(\(r_i\) 表示 \(i\) 的回文半径)。正确性显然。(注意因为我们只考虑偶回文串,所以我们只枚举以特殊字符 #
(Manacher 时插入的特殊字符)为回文中心的贡献)
判断剩下的部分是否回文就直接用中心的回文半径判即可。最长偶回文前缀就枚举的时候顺便维护一下即可。
#include <bits/stdc++.h>
template <class T>
inline void read(T &x)
{
static char ch;
while (!isdigit(ch = getchar()));
x = ch - '0';
while (isdigit(ch = getchar()))
x = x * 10 + ch - '0';
}
template <class T>
inline void relax(T &x, const T &y)
{
if (x < y)
x = y;
}
template <class T>
inline void tense(T &x, const T &y)
{
if (x > y)
x = y;
}
const int MaxN = 1e4 + 5;
int n, m, ans;
int a[MaxN], s[MaxN];
inline void solve(int *s, int n)
{
static int t[MaxN], r[MaxN], m;
static int max_suf[MaxN];
m = 0;
for (int i = 1; i <= n; ++i)
{
t[(i << 1) - 1] = 0;
t[i << 1] = s[i];
}
m = n << 1 | 1, t[m] = 0;
t[0] = -1, t[m + 1] = -2;
int rit = 0, p = 0;
for (int i = 1; i <= m; ++i)
{
if (rit > i)
{
int j = (p << 1) - i;
r[i] = std::min(r[j], rit - i);
}
else
r[i] = 1;
while (t[i - r[i]] == t[i + r[i]])
++r[i];
if (i + r[i] > rit)
{
rit = i + r[i];
p = i;
}
max_suf[i] = 0;
}
rit = 1;
for (int i = 1; i <= m; i += 2)
{
for (int j = i + r[i] - 1; j >= rit && j > i; --j)
relax(max_suf[j], (j - i) << 1 | 1);
relax(rit, i + r[i]);
}
int cur_pre = 0;
for (int i = 2; i <= n; i += 2)
{
bool flg = false;
if (r[i + 1] > i)
{
flg = true;
cur_pre = i + 1;
}
if (max_suf[i << 1])
{
int cur = i - (max_suf[i << 1] >> 1) - 1;
if (r[cur + 1] > cur)
flg = true;
}
if (cur_pre && r[cur_pre + i] > i - cur_pre)
flg = true;
ans += flg;
}
}
int main()
{
freopen("naive.in", "r", stdin);
freopen("naive.out", "w", stdout);
read(n);
for (int i = 1; i <= n; ++i)
read(a[i]);
for (int i = 1; i < n; ++i)
{
m = 0;
int l = i, r = i + 1;
while (l >= 1 && r <= n)
{
s[++m] = a[l];
s[++m] = a[r];
--l, ++r;
}
solve(s, m);
}
std::cout << ans << std::endl;
return 0;
}
回文树做法
用 Hash 判回文。求每个前缀的最长偶回文前缀只要枚举的时候维护一下即可。
然后求最长偶回文后缀只要在回文树上找到对应点的 fail 链上深度最大的偶回文串即可,这个也可以在构造回文树的时候顺带维护一下。
(代码懒得写)