Manacher 算法浅谈
\(Zero.\) \(~~\)前言杂谈
认识我的人都喜欢叫我马拉车,如今,马拉车来浅谈 Manacher 了(不就是某天打板子的时候打错了吗,不就是啪啪打脸了吗)。
首先大家需要知道,Manacher 不是很常考,但是也是一项必备的算法。当遇到回文串之类的问题时,别人辛辛苦苦打一堆哈希,你用 Manacher 算法两个并列的循环就搞定了。而且有些回文自动机 PAM 的题有时候可以用 Manacher,不一定要学那么厉害的算法。
这样说下来,Manacher 一下子感觉有些高大上了。
放假前最后一天,GM 问初三下来的学没学过 Manacher,我顿时瞪大了眼睛(虽然某些人反复强调马拉车来调侃我。。),但我也非常清楚,Manacher 作用不大,短小精悍,题库里面就有 \(18\) 道公开的题目,比那啥四边形不等式好像还要火一些。所以此帖就是单纯想要水一下 Manacher 算法,如果你看了,在未来某些时刻学到这个东西时,你也许就会更强一些。
GM说了下半期讲树链剖分和分块,但是我却自己写着 Manacher 的blog。。
\(One.\) \(~~\)作用与思想
假如给你一个字符串,让你求这个字符串里面最长的回文串,你通常会怎么求。没学过 Manacher 的多半都会 \(O(n^3)\) 暴力枚举,或者学了哈希,区间 DP 的可以更快速地解决,而我们的 Manacher 可以优化到线性的时间复杂度,即 \(O(n)\) 去处理出答案,洛谷上 \(1.1e7\) 的数据都可以快速过掉。
当然有些时候,Manacher 可能会伴随二分,DP 等其他知识,所以做题的时候不能光想着 Manacher,还要结合实际进行分析。
那么 Manacher 求出最长回文子串的思想是什么呢??
首先我们需要有一个基础的 \(O(n^2)\) 操作,这个操作就是遍历 \(1,2…n-1,n\) 的所有字符,假设当前的字符下标为 \(i\),则我们向左右两边进行扩展,直到 \(S_{i-k}≠S_{i+k}\),此时 \(2k+1\) 就是以 \(i\) 为中点的最长回文串,而求出整个字符串的最长回文串,我们可以枚举每一个 \(i\) 去比最大值。当然,如果你要找长度为偶数的回文串,那么就是以 \(i-1\) 和 \(i\) 为两个中点进行扩展。
而这种做法肯定是会被 \([10^4,10^7]\) 的数据卡掉的,于是我们需要进行合理的优化。接下来就是 Manacher 思想了。
我们上述做法的劣势就是,以 \(i\) 为中点的回文串半径总是从 \(0\) 开始,这样无疑会增大我们的时间复杂度。我们考虑维护一个区间 \([l,r]\),其中 \(r\) 满足是 \([1,i-1]\) 枚举过后最靠后的回文串的右端点,\(l\) 就是对应的回文串的左端点。例如 abacabad
中,回文串有 aba
,abacaba
,而 abacaba
的右端点最靠后,此时 \(r=7\),对应的 \(l=1\)。
维护这个区间肯定是有好处的。如果 \(i\leq r\),那么我们也许可以从这个 \([l,r]\) 找一下优化,设 \(mid=\dfrac{l+r}{2}\),很明显 \(mid\) 为一个对称轴,我们再做 \(j\) 关于 \(mid\) 与 \(i\) 对称。(从图感性理解一下)
首先可以证明 \(i>mid\) 是一定的,因为这里的 \(mid\) 就是前面处理过的一个中点,而如果是长度为偶数的字符串,那就更容易证明了,所以由 \(mid∈[1,i-1]\) 可推出。这样就说明 \(j<i\),所以设 \(d_x\) 为以 \(x\) 为中点的回文串的半径长度,考虑从 \(d_j\) 和 \(d_i\) 找到关联。联系我们的区间 \([l,r]\),根据定义可知 \(S_{l…r}\) 也是一个回文串。那么就可以容易地得到 \(S_{l…mid}=S_{r…mid}\)。又因为 \(i,j\) 对称,所以有 \(d_j=d_i\)。把 \(j\) 转换一下,则结论为 \(d_i=d_{l+r-i}\)。
但是我们貌似还需要考虑到一个问题,假如 \(j-d_j<l\),那么就意味着 \(i+d_i>r\),而我们目前只可以保证 \(S_{l…r}\) 是回文串,\(r\) 后面的部分我们暂时无法判断回文信息。如果我们直接赋值为 \(d_j\),那么就有可能出现错误答案。比如 ababacabad
,我们的区间为 \([3,9]\),此时 \(i=9\),得出 \(j=3\),而很明显 \(d_j=3\),即字符串 ababa
,如果 \(d_i=3\),很明显是错误的。所以我们还需要保证初始不超过边界。
所以最终结论:
所以我们从 \(d_i\) 的最小值开始扩展,每次如果 \(S_{i-d_i}=S_{i+d_i}\),就给 \(d_i\) 加一。
而回文串分为奇数长度和偶数长度,上文的做法均为奇数长度,对于偶数长度,我们的思路一致,但是回文串中点的定义是 \(i\) 和 \(i-1\),并不只是 \(i\),所以判断两端相等的写法会稍微不一样。
其时间复杂度可能会发生争议,而我是从 wiki 上面学的 Manacher,所以这里大家可以去 wiki 得到时间复杂度的证明。
\(Two.\) \(~~\)模板代码
模板代码如下:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1.1e7+5;
int d1[MAXN],d2[MAXN];
char s[MAXN];
int maxx=-1;
int len;
int main()
{
scanf("%s",s);
len=strlen(s);
for(int i=0,l=0,r=-1;i<len;i++)
{
int k=(i>r)?(1):(min(d1[l+r-i],r-i+1));
while(i-k>=0&&i+k<len&&s[i-k]==s[i+k]) k++;
d1[i]=k--;
maxx=max(maxx,d1[i]*2-1);
if(i+k>r) l=i-k,r=i+k;
}
for(int i=0,l=0,r=-1;i<len;i++)
{
int k=(i>r)?(0):(min(d2[l+r-i+1],r-i+1));
while(i-1-k>=0&&i+k<len&&s[i-1-k]==s[i+k]) k++;
d2[i]=k--;
maxx=max(maxx,d2[i]*2);
if(i+k>r) l=i-k-1,r=i+k;
}
printf("%d",maxx);
return 0;
}
\(Three.\) \(~~\)习题
年前几天做了几道小题,现在我来分享一下成果(均为个人做法,可能和题解做法有不同)。
最长双回文串
老套板板题。
首先不难想到标记法。我们知道一个双回文串需要一个回文串的右端点和一个回文串的左端点并在一起,那么我们只需要求出每个回文串的左右端点,进行标记法,最后处理一下就可以了。
设 \(dp_i\) 为以 \(i\) 结尾的回文串的最大长度,\(f_i\) 为以 \(i\) 开头的回文串的最长长度。然后按照 Manacher 的遍历,在过程中给每次的 \(dp_i\) 和 \(f_i\) 进行更新。
即:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
int d1[MAXN],d2[MAXN];
char s[MAXN];
int dp[MAXN],f[MAXN];
int maxx=2;
int len;
int main()
{
scanf("%s",s);
len=strlen(s);
for(int i=0;i<len;i++) dp[i]=f[i]=1;
for(int i=0,l=0,r=-1;i<len;i++)
{
int k=(i>r)?(1):(min(d1[l+r-i],r-i+1));
while(i-k>=0&&i+k<len&&s[i-k]==s[i+k]) dp[i+k]=max(dp[i+k],k*2+1),f[i-k]=max(f[i-k],k*2+1),k++;
d1[i]=k--;
if(i+k>r) l=i-k,r=i+k;
}
for(int i=0,l=0,r=-1;i<len;i++)
{
int k=(i>r)?(0):(min(d2[l+r-i+1],r-i+1));
while(i-1-k>=0&&i+k<len&&s[i-1-k]==s[i+k]) dp[i+k]=max(dp[i+k],k*2+2),f[i-k-1]=max(f[i-k-1],k*2+2),k++;
d2[i]=k--;
maxx=max(maxx,d2[i]*2);
if(i+k>r) l=i-k-1,r=i+k;
}
for(int i=0;i<len-1;i++) maxx=max(maxx,dp[i]+f[i+1]);
cout<<maxx;
return 0;
}
可这是错误的但是可以通过原题数据的。。
为什么我强调自己的想法是错误的?
因为我写博客的时候突然把我的代码 Hack 掉了。
比如 cdcabaccabacdcabac
,正确的答案是 \(17\),我输出了 \(16\),原因是我只考虑到了 Manacher 时,每次 \(d_i\) 从最小值开始扩展,但是忽略了 \(d_i∈[0,\min\{d_{l+r-i},~r-i+1\})\) 时带来的代价。在这个数据中,明显发现因为维护了区间 \([8,18]\),\(16\) 的对称点是 \(10\),显然 \(d_{10}>2\),所以忽略掉了 aba
,因此无法连接之前的 cdcabaccabacdc
,导致 cdcabaccabacdcaba
无法被统计。
所以我的想法仅供参考,但如果你想贺掉这道题,你可以用我的垃圾思路去交,最好还是自己想一下或者去看一下别人的正解。
写博客的我已因为 Hack myself 而哭晕在卧室。
绿绿与串串
乍一看可能看不出来这是一道 Manacher,但是因为有这样的翻转操作,我们也可以轻松想到回文串,然后进一步进行分析,这就很明显和 Manacher 扯不开关系。
本题的难点,就是该怎么判断一个串可以通过多次翻转得到目标串?
这个其实非常简单,以 qwqwq
为例,假设我们已经求出了 qwq
是一个答案,那么我们再将 qwq
设为目标串,根据观察,qw
可以通过一次翻转得到 qwq
,那么同样的,qw
可以通过两次翻转得到 qwqwq
。
想到这里,我们就可以定义一个 \(vis\) 数组判断 \([1,i]\) 的串是否可以翻转成目标串,然后从大到小进行操作,这样可以保证先判断更大的字符串 \(R\),使它成为新的目标串,再利于判断 \(|R_2|<|R|\) 的字符串。
而判断长度大的 \(R\) 能否翻转成目标串,我们可以推一下式子,也就是其子串末尾节点的下标加上回文串的半径 \(d_i\),如果它达到整个字符串末尾,就说明子串可以翻转成目标串。不要忘了保证字符串的开头是 \(1\)。即判断条件为:
对于通过多次翻转的子串,我们可以用同样的方式,更换目标串,然后套用同样的式子进行判断。
而且不难发现,所有的翻转的回文串都是奇数长度,所以我们可以省略跑偶数长度的 Manacher,只需要奇数长度的 Manacher 就可以了。
重点代码如下:
for(int i=len-1;i>=0;i--)
{
if(d1[i]+(i-1)==len-1) vis[i]=1;
if(vis[d1[i]+i-1]&&i+1==d1[i]) vis[i]=1;
}
Casting Spells
Manacher 明显的板板题。
首先不难想到 Manacher 只用跑偶数长度的,人家题目概念写的清清楚楚,奇数长度连分都分不了。然后又不难分析出 \(4~|~2\times d_i\),因为这四个子串的长度一样,并且是并列组合的,所以长度必须是 \(4\) 的倍数。化简一下就是 \(2~|~d_i\)。
接着就是判断一个偶数长度回文串是否满足条件。我们可以将这个回文串拆成 \([l,mid-1]\) 和 \([mid,r]\) 两个字符串,由题意,我们需要判断 \(S_{l…mid-1}\) 和 \(S_{mid…r}\) 也都是回文串。那么我们就不难想到找到两个回文子串的中点 \(id_{1,2}\),接着根据 \(d_{id}≥\dfrac{d_{mid}}{4}\) 的条件进行判断。
说得简单,做起来貌似就有问题了。
还是第一题讲的问题,我们直接按照 Manacher 的方式扩展,就会忽略掉中间的一些可能给最优解带来代价的回文子串。假如我们直接按照求出来的最大回文串进行 \(d_{id}≥\dfrac{d_{mid}}{4}\) 的判断,那么就不能保证正确性。倘若 \(d_{id}<\dfrac{d_{mid}}{4}\),它并不能代表比 \(d_{mid},~d_{id}\) 更小的半径长度不能符合条件。
所以我们不能在处理过后用最长的结果进行判断,而是在 Manacher 的过程中进行处理。但是就会和第一题的题外Hack性质一样。所以这样的修改只能是得到更多的分数(或者说可能证明这样不会影响答案,反正我还没有进行 Hack myself)。
但是改之后我们可以想到,不用求出 \([mid,r]\) 的中点,因为 \([l,r]\) 就是回文串,所以只要求出 \([l,mid-1]\) 是回文串就行了,和 Manacher 优化思想是差不多的。
老话:如果你想用错误代码贺一下原题,你可以按照我的思路来写。
for(int i=0,l=0,r=-1;i<len;i++)
{
int k=(i>r)?(0):(min(d2[l+r-i+1],r-i+1));
while(i-1-k>=0&&i+k<len&&s[i-1-k]==s[i+k])
{
if(!(k&1))
{
int id1=(i*2-k)>>1;
if(d2[id1]>=k>>1) maxx=max(maxx,k*2);
}
k++;
}
d2[i]=k--;
if(!(d2[i]&1))
{
int id1=(i*2-d2[i])>>1;
if(d2[id1]>=d2[i]>>1) maxx=max(maxx,d2[i]*2);
}
if(i+k>r) l=i-k-1,r=i+k;
}
「JSOI2016」扭动的回文串
然而是 yzh 告诉了我这道题然后我从上午大课间一直想到了晚上最后才发现有一个非常智慧的垃圾结论导致我心中无限个草泥马飞驰而过虽然最后还是打完了这道题。。
前面两个情况非常的板,就是分别在 \(a,b\) 上跑 Manacher。
第三种情况是本题的难点,我们不难想到,要讨论回文串中心是在 \(a\) 上还是在 \(b\) 上。
继而可以想到枚举这个中心 \(mid\) 的位置。如果我们令 \(mid\) 在 \(a\) 上,那么我们可以先找到在 \(a\) 上面的一个回文串 \(s\),设 \(l,r\) 分别为 \(s\) 的左右端点。接着二分找到最大的 \(len\) 满足 \(a_{l-1,\dots,l-len}=b_{r,r+len-1}\)。那么 \(|s|+2\times len\) 就可以作为一个可能的答案。为了方便下文表示,我们令 \(f(l,r)\) 为最后得到的 \(|s|+2\times len\)。
然而枚举每一个 \(s\) 显然要废掉。因此我们需要一个结论:设以 \(mid\) 为中心的最大回文串的左右端点为 \(L,R\),那么 \(f(L,R)\) 一定是最优解。
证明:我们设另一个以 \(mid\) 为中心的回文串的左右端点为 \(l,r\)。如果 \(f(l,r)\leq R-L+1\),那么很明显选择 \(f(L,R)\) 一定不劣。而如果 \(f(l,r)>R-L+1\),那么就是下面这种情况:
假设表格中第一列下标为 \(r\),最后一列下标为 \(R\)。由于 \(f(l,r)>R-L+1\),那么最终的扭动回文串一定会走到右下角的 f
。但是如果我们选择 \(f(L,R)\),我们发现它肯定也能直接走到 f
。因此两种情况是等价的,那我们直接选择 \(f(L,R)\) 不就节省了很多时间吗?
于是具体的思路就出来了:
-
用 Manacher 处理 \(a,b\) 中的最长回文串。
-
钦定扭动回文串的中点 \(mid\) 在 \(a\) 上,找到以 \(mid\) 为中心的最长回文串,左右端点为 \(L,R\)。然后在这个串的前面和后面一起二分求出最长的公共子串,最后进行统计。
-
钦定中点 \(mid\) 在 \(b\) 上,和上面的方法是一样的。
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 5;
const int base = 179;
int n, res;
char a[MAXN], b[MAXN];
int d1[MAXN], d2[MAXN];
unsigned long long Hash1[MAXN], Hash2[MAXN], mul[MAXN];
int main() {
cin >> n >> (a + 1) >> (b + 1);
mul[0] = 1;
for (int i = 1; i <= n; i++) mul[i] = mul[i - 1] * base;
for (int i = 1; i <= n; i++) Hash1[i] = Hash1[i - 1] * base + a[i];
for (int i = n; i >= 1; i--) Hash2[i] = Hash2[i + 1] * base + b[i];
for (int i = 1, l = 1, r = 0; i <= n; i++) {
int k = (i > r) ? (1) : (min(d1[l + r - i], r - i + 1));
while (i - k >= 1 && i + k <= n && a[i - k] == a[i + k]) k++;
d1[i] = k--, res = max(res, d1[i] * 2 - 1);
if (i + k > r)
l = i - k, r = i + k;
}
for (int i = 1, l = 1, r = 0; i <= n; i++) {
int k = (i > r) ? (0) : (min(d2[l + r - i + 1], r - i + 1));
while (i - 1 - k >= 1 && i + k <= n && a[i - 1 - k] == a[i + k]) k++;
d2[i] = k--, res = max(res, d2[i] * 2);
if (i + k > r)
l = i - k - 1, r = i + k;
}
for (int i = 1; i <= n; i++) {
int k = i + d1[i] - 2, j = i - d1[i] + 1;
int l = 1, r = min(j - 1, n - k), id = 0;
while (l <= r) {
int mid = (l + r) / 2;
if (Hash1[j - 1] - Hash1[j - mid - 1] * mul[mid] == Hash2[k + 1] - Hash2[k + mid + 1] * mul[mid])
id = mid, l = mid + 1;
else
r = mid - 1;
}
res = max(res, (d1[i] + id) * 2 - 1);
k = i + d2[i] - 2, j = i - d2[i];
l = 1, r = min(j - 1, n - k), id = 0;
while (l <= r) {
int mid = (l + r) / 2;
if (Hash1[j - 1] - Hash1[j - mid - 1] * mul[mid] == Hash2[k + 1] - Hash2[k + mid + 1] * mul[mid])
id = mid, l = mid + 1;
else
r = mid - 1;
}
res = max(res, (d2[i] + id) * 2);
}
memset(d1, 0, sizeof(d1)), memset(d2, 0, sizeof(d2));
for (int i = 1, l = 1, r = 0; i <= n; i++) {
int k = (i > r) ? (1) : (min(d1[l + r - i], r - i + 1));
while (i - k >= 1 && i + k <= n && b[i - k] == b[i + k]) k++;
d1[i] = k--, res = max(res, d1[i] * 2 - 1);
if (i + k > r)
l = i - k, r = i + k;
}
for (int i = 1, l = 1, r = 0; i <= n; i++) {
int k = (i > r) ? (0) : (min(d2[l + r - i + 1], r - i + 1));
while (i - 1 - k >= 1 && i + k <= n && b[i - 1 - k] == b[i + k]) k++;
d2[i] = k--, res = max(res, d2[i] * 2);
if (i + k > r)
l = i - k - 1, r = i + k;
}
for (int i = 1; i <= n; i++) {
int k = i + d1[i] - 1, j = i - d1[i] + 2;
int l = 1, r = min(j - 1, n - k), id = 0;
while (l <= r) {
int mid = (l + r) / 2;
if (Hash1[j - 1] - Hash1[j - mid - 1] * mul[mid] == Hash2[k + 1] - Hash2[k + mid + 1] * mul[mid])
id = mid, l = mid + 1;
else
r = mid - 1;
}
res = max(res, (d1[i] + id) * 2 - 1);
k = i + d2[i] - 1, j = i - d2[i] + 1;
l = 1, r = min(j - 1, n - k), id = 0;
while (l <= r) {
int mid = (l + r) / 2;
if (Hash1[j - 1] - Hash1[j - mid - 1] * mul[mid] == Hash2[k + 1] - Hash2[k + mid + 1] * mul[mid])
id = mid, l = mid + 1;
else
r = mid - 1;
}
res = max(res, (d2[i] + id) * 2);
}
cout << res;
return 0;
}
简单的字符串
神仙题,麻了。。。这道题我首先是想了很久想到了接近正解的思路,只差临门一脚,但是最后的某个处理我问了别人,貌似都讲不清楚改怎么处理。。啊因此只能看题解了。个人认为这个结论挺难证明的,这里就解释一下吧。。
PART Ⅰ 本质分析
首先看了题,貌似想不到和回文串有什么关系。我们先观察一下其中的规律,不难发现同构的含义:在两个环上各选择一个起点,然后按照指定顺序遍历每一个节点,最终各自得到一个序列,有一种选择起点的方法满足两个序列相同,那么就是同构。
根据题目,我们可以想到枚举中点 \(mid\)。再结合对同构的分析,最简单的情况莫过于 \(a_{l\dots mid-1}\) 和 \(a_{mid\dots r}\) 本身就是相同的。我们把这个称为基础情况。
进一步想,如果 \(a_{l\dots mid-1}=\{5,6,7,1,2,3,4\}\),\(a_{mid\dots r}=\{1,2,3,4,5,6,7\}\),我们发现只要将前者分成两组 \(\{5,6,7\},\{1,2,3,4\}\),然后交换顺序并拼接在一起,就会得到后者。这启示我们可以找前面半段的分组方式(最多分 \(2\) 组),看能否变为后半段。
PART II 思维转换
定义前半段按照下标顺序形成的子段为 \(A\),后半段为 \(B\)。
我们先考虑基础情况。可以发现对于每一个 \(i\) 都满足 \(A_i=B_i\)。这种对应关系可以很奇妙地引申到一个转换,就是我们定义一个新的序列 \(C\),然后让 \(i\) 从 \(1\to n\) 枚举,每次将 \(A_{n-i+1}\) 和 \(B_i\) 依次放入 \(C\) 的末尾。比如 \(A=\{1,2,3\},B=\{4,5,6\}\),那么 \(C\) 就是 \(\{3,4,2,5,1,6\}\)(注意这个只是例子,它并不是基础情况!!)。根据基础情况的性质,我们可以发现 \(C\) 一定是一个长度为偶数的回文串。这个就不需要赘述了吧。。
所以我们就可以阴差阳错地想到了和正解息息相关的思路。(其实如果告诉你这是个回文串题,说不定可能会想得到,但是你不说,我打死想不到。。)
因此对于基础情况,我们直接判断 \(C\) 是否是长度为偶数的回文串就行了。
然后就拓展到其他情况。在第一部分,我们知道了本质应该是去给 \(A\) 分成 \(2\) 个子串,然后通过重新拼接,看能否对的上 \(B\)。
假设我们将 \(A\) 依次分成了长度分别为 \(l_1,l_2\) 的两个子串,我们其实不难发现 \(A_{l_1+1\dots l_1+l_2}=B_{1\dots l_2}\)(也挺容易证明的,自己手玩吧)。这个不就是一个基本情况吗!?我们还可以推出,如果我们按照上面的方法构造 \(C\),那么 \(C\) 的前面 \(2\times l_2\) 项一定是一个长度为偶数的回文串。
同理我们可以知道 \(A_{1\dots l_1}=B_{l_2+1\dots l_2+l_1}\),并且 \(C_{2\times l_2+1\dots 2\times (l_1+l_2)}\) 也是一个长度为偶数的回文串。
因此这种情况的 \(C\) 就是两个长度为偶数的回文串拼接在一起!!!
然后你就懂了这题应该怎么搞了。就是先枚举中点 \(mid\),然后把整体 \(C_0\) 序列给提前弄出来。这样每次我们考虑的区间 \([l,r]\) 对应的 \(C\) 序列就是 \(C_0\) 的一个前缀了。然后我们就判断这个前缀能否可以看作是两个长度为偶数的回文串拼接在一起,只要我们不是 \(O(n)\) 判断就行了。
PART III 结论
然后我不会判断。。。这就是我差的那个临门一脚。。。
先抛出结论吧:对于一个长度为偶数的 \(S\),若其能看作是两个长度为偶数的回文串 \(x,y\) 拼接在一起,那么肯定有一种方案满足 \(x\) 为最长回文前缀或者 \(y\) 为最长回文后缀。(这里的回文都定义为长度为偶数的回文串)。
证明:
令 \(s^R\) 为 \(s\) 前后翻转后得到的字符串。
设 \(S=x_1y_1=x_2y_2=x_3y_3\),其中 \(x_2,y_2\) 为长度为偶数的回文串,\(x_1\) 是 \(S\) 的最长的偶数长度的回文前缀,\(y_3\) 是 \(S\) 的最长的偶数长度的回文后缀。考虑反证,若结论不成立,那么 \(y_1,x_3\) 都不是回文串。
此时有 \(|x_1|>|x_2|>|x_3|,|y_1|<|y_2|<|y_3|\)。
设 \(x_1=x_2p\),那么 \(p\) 肯定是 \(y_2\) 的前缀,\(p^R\) 为 \(y_2\) 的后缀,接着有 \(p^R\) 为 \(y_3\) 的后缀。因为 \(y_3\) 是回文串,所以 \(p\) 也是 \(y_3\) 的前缀。所以 \(x_3p\) 为 \(x_1\) 的前缀。
由于 \(x_2\) 是 \(x_1\) 的前缀,所以 \(x_2^R\) 是 \(x_1\) 的后缀。
-
若 \(2\times |x_2|<|x_1|\),则很明显 \(x_2\) 是 \(p^R\) 的前缀,由于 \(x_1=x_2p=p^Rx_2^R=p^Rx_2\),进而得到 \(|p|\) 是 \(x_1\) 的一个周期,即 \(x_1\) 是 \(p^Rp^Rp^Rp^R\dots\) 的一个前缀。
-
若 \(2\times |x_2|≥|x_1|\),手玩都可以知道 \(|p|\) 是 \(x_1\) 的一个周期。
综上,\(x_1\) 是 \(T=p^Rp^Rp^Rp^R\dots\) 的前缀。由于 \(x_3\) 为 \(x_1\) 的前缀,所以 \(x_3\) 也是 \(T\) 的前缀。同理 \(x_3p\) 为 \(T\) 的前缀。
由于 \(x_3p,p^Rx_3^R\) 都是 \(x_1\) 的子串,所以 \(|p|\) 是两者都有的周期。显然 \(|p^R|=|p|\),因为 \(|p|\) 是 \(p^Rx_3^R\) 的周期,所以 \(|p|\) 也是 \(x_3^R\) 的周期,即 \(x_3^R\) 是 \(T\) 的前缀。
因为 \(x_3,x_3^R\) 都是 \(T\) 的前缀,且 \(|x_3|=|x_3^R|\),所以 \(x_3=x_3^R\),即 \(x_3\) 是一个回文串。和假设矛盾。因此结论成立。
那么我们判断一个 \(S\) 是否合法只需要令 \(x\) 为最长回文前缀,或者 \(y\) 为最长回文后缀,然后对于这两种情况看是否合法。如果不合法,那么其他方法肯定也不行。
因此我们只需要维护当前的最长回文前缀,并且在 Manacher 的时候计算以某个端点结尾的最长回文串,这样判断的时候就比较容易了。
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5005;
int n;
int a[MAXN];
int stk[MAXN], cnt;
int d2[MAXN], f[MAXN];
vector<int> vec[MAXN];
unordered_map<int, int> mp;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
int res = 0;
for (int t = 2; t <= n; t++) {
cnt = 0;
int l = t - 1, r = t;
while (l >= 1 && r <= n) stk[++cnt] = a[l], stk[++cnt] = a[r], l--, r++;
memset(f, 0, sizeof(f)), memset(d2, 0, sizeof(d2));
for (int i = 1, l = 1, r = 0; i <= cnt; i++) {
int k = (i > r) ? (0) : (min(d2[l + r - i + 1], r - i + 1));
while (i - 1 - k >= 1 && i + k <= cnt && stk[i - 1 - k] == stk[i + k]) k++;
d2[i] = k--, f[i + d2[i] - 1] = max(f[i + d2[i] - 1], d2[i] * 2);
if (i + k > r)
l = i - k - 1, r = i + k;
}
for (int i = cnt; i >= 1; i--) f[i] = max(f[i], f[i + 1] - 2);
int maxx = 0;
for (int i = 2; i <= cnt; i += 2) {
if (f[i] == i)
maxx = i;
if (d2[(i + maxx) / 2 + 1] >= (i - maxx) / 2)
res++;
else if (d2[(i - f[i]) / 2 + 1] >= (i - f[i]) / 2)
res++;
}
}
cout << res;
return 0;
}
/*
16
1 2 1 1 2 2 2 2 2 2 2 2 1 1 2 1
*/
\(Four.\) \(~~\)总结
反正,Manacher 过一道题确实不难,但是想要写出完全不会被 Hack myself 的代码确实挺难的(for me)。所以没事不要看我的代码,我自己都把自己 Hack 傻了。但是大致思路应该和正解偏差不大,只需要在实现上花点功夫改正就行了。
话说 Manacher 难题挺多的。
如果有兴趣,可以去看一下 回文自动机PAM,就是储存字符串中所有回文串的强强的自动机。洛谷板板题就是求以 \(i\) 结尾的字符串的个数,但是是在线的输出答案,即一遍输入一遍输出当前答案,所以想用 Manacher 水题的人就不要想了。
目前是觉得 Manacher 上正式考场的几率不大,所以就当 \(2023\) 年的开胃小菜了(反正以后会讲,我提前学了不是乱脱控打代码??不会的啦,AC 很蓝的啦)。