Manacher 学习笔记
\(\text{Manacher}\) 学习笔记
定义
所谓回文串,指的是对于一个字符串 \(s\),若它的长为 \(n\),下标从 \(1\) 到 \(n\),如果 \(\forall i\in [1,n],s_i=s_{n+1-i}\),那么字符串 \(s\) 是一个回文串。
给定一个字符串 \(s\),求解它总共的回文子串个数。对于这一类问题的求解,我们发现,因为一个字符串的回文串最差是 \(O(n^2)\) 个,因此,我们貌似没有什么好的方法去优化算法的复杂度。但是,我们可以换一种更加紧凑的统计方式。
我们认为对于一个字符串 \(s\) 的子串 \(s[i\dots j]\),它的对称中心为 \(\lfloor\frac{i+j+1}{2}\rfloor\),那么我们可以统计对于每一个对称中心,长度为奇数、偶数的字符串分别有多少个,记为 \(d_1,d_2\),将两个数组中的所有值相加起来的结果即为答案。
这样统计的正确性是显然的,那么问题就在于如何求解了。
实现
暴力枚举
首先,对于 \(d_1,d_2\) 有一个性质:对于一个对称中心 \(i\),假设共有 \(k\) 个以它为中心的回文串,那么这些回文串的右端点一定是 \(i,i+1,\cdots,i+k-1\)。
最容易想到的方法无疑于暴力枚举,对于每一个对称中心,向两边枚举,直到两端不相同,将统计到的答案记入数组中,这样的复杂度是 \(O(n^2)\) 的。
\(\text{Manacher}\)
与暴力枚举不同的是,\(\text{Manacher}\) 致力于找到对于一个对称中心 \(i\),通过之前已经求解出的 \(d_1\) 数组,通过较优的复杂度处理出对应的结果。
我们考虑枚举到 \(i\) 时,整个字符串中右端点最靠右的回文串的下标范围为 \([l,r]\)。当 \(i\le r\) 时,我们可以得知 \(s_i=s_{l+r-i}\)。这是回文串的定义,更普遍的,\(\forall j\in[i-r,r-i],s_{i+j}=s_{l+r-i-j}\)。因为这两个位置是回文串 \([l,r]\) 的对应位置。现在我们目光聚集于以 \(i\) 为对称中心的回文串,我们发现 \(s_{i+j}=s_{i-j}\) 成立当且仅当 \(s_{l+r-i-j}=s_{l+r-i+j}\),这等价于两个位置关于 \(l+r-i\) 对称的点相等,那么我们发现,\(l+r-i\) 为对称中心的回文串有多少个,那么以 \(i\) 对称中心的回文串就理论有多少个。
为什么是理论多少个,因为当以 \(l+r-i\) 为对称中心的最长回文串超出了最右回文串的范围时,便不能保证答案的正确性,因为此时的 \(s_{l+r-i-j}\ne s_{i+j}\)。所以我们需要取 \(\min(d_1[l+r-i],r-i+1)\) 作为初始的最长长度,接着不断向两侧暴力拓展。
当 \(i>r\) 时,对称中心不在回文串中,因此只能暴力。
接着考虑复杂度的问题,对于 \(i\le r\) 的情况,容易证明不会将 \(k\) 增加。而对于 \(i>r\) 的情况,\(k\) 增加多少,就会将最右回文串向右移动多少位,因为最右回文串的右端点不会减小,并且最大为 \(n\),因此复杂度是 \(O(n)\) 的。
以上的讨论均针对长度为奇数的回文串,长度为偶数的回文串的思路类似。两者代码不同,但我们可以通过一个预处理,在一个基本相同的时间复杂度内用相同的代码求解。
考虑将所有的回文串的长度均变为奇数的方法:在字符串的开头、结尾和任意两个字符串之间均添加一个相同字符,如 \(\#\),这样原来长度为 \(n\) 的回文串的长度会变为 \(2n+1\),一定为奇数。此时可以只通过求解长度为奇数的回文串得到正确答案。值得注意的是,如此处理完成后,回文串 \(s\) 和回文串 \(\#s\#\) 在原串中对应的回文串相等,因此应将答案整体除以 \(2\)。更具体的,设 \(\text{Manacher}\) 处理出的数组为 \(d\) ,那么 \(d_1\) 与对应的 \(d\) 的关系为 \(d=2d_1\),\(d_2\) 与对应的 \(d\) 的关系为 \(d=2d_2+1\)。
代码
void Manacher(){
int l=0,r=-1;//初始化
for(int i=0;i<n;i++){
int k=(i>r)?1:min(d[l+r-i],r-i+1);//判断已知最长回文串的长度
while(i-k>=0&&i+k<n&&s1[i-k]==s1[i+k])k++;//暴力枚举
d[i]=k--;//记录答案
if(i+k>r){
l=i-k;
r=i+k;
}//更新最右回文串
}
return ;
}
应用
最长回文子串
之前讲解了利用后缀数组 \(O(n\log n)\) 求解的办法,接下来讲解更为迅猛的 \(\text{Manacher}\) 法。
在我们求解出 \(d\) 数组后,我们容易得到以每一个位置为对称中心的最长回文子串的长度:对于长度为奇数的回文串,它的最长长度为 \(2d_1-1=d-1\);对于长度为偶数的回文串,它的最长长度为 \(2d_2=d-1\)。因此对 \(d\) 数组的值取最大后将答案减 \(1\) 即可。复杂度是 \(O(n)\) 的。
后记
事实上,不只是对回文串,对具有回文串类似的性质,即满足 \(\forall i\in[1,n],\text{Check}(s_i,s-n+1-i)=\text{True}\) 的定义,都可以利用 \(\text{Manacher}\) 求解。下给出对于普遍的性质的标准代码。
code
void Manacher(){
int l=0,r=-1;
for(int i=0;i<n;i++){
int k=(i>r)?Check(i,i):min(d[l+r-i],r-i+1);
while(i-k>=0&&i+k<n&&Check(i-k,i+k))k++;
if(k>0)d[i]=k--;
if(i+k>r){
l=i-k;
r=i+k;
}
}
return ;
}