【学习笔记】Manacher

思路

暴力统计回文串个数

很简单 \(\Theta(n)\) 枚举左端点, \(\Theta(n)\) 枚举右端点,然后暴力判断,复杂度 \(\Theta(n)\)

当然,可以优化,判断那部分用哈希搞一搞以后就是 \(\Theta(n^2)\)

另一个思路

\(\Theta(n)\) 找到每一个对称中心,然后暴力向两边拓,最坏 \(\Theta(n^2)\) ,然而这就是 Manacher 的基础。

优化

我们发现,每遍历到一个新的对称中心,都需要再两边拓一遍,然而这就必然会浪费之前花费大量时间求出来的信息,所以要考虑用之前的信息来转移该点的答案。

由于之前求的信息均与对称相关,我们考虑使用有关对称的信息来更新答案。

怎么使用呢?我们可以将这个点对称到已经求过的地方来统计答案,而为了尽可能利用更多的信息,应该选择 { [ ( 从对称中心出发加上对称半径 ) 能够够到的最右端 ] 最靠右的 } 点(有点绕,加了一些括号辅助理解),暂且将以该点为对称中心的回文串称为 s ,将该点称为 id。

对于所求点 i ,不难证明其一定在 id 右侧,不过 s 可能无法覆盖到 i 点,但是这里先假设 s 能够覆盖到 i 点。

显然,如果将 i 依据 id 对称到 s 串左侧对应位置(称为 j),就会发现一个如下图的现象:

image

显然,以 j 为对称中心的回文串本来是 ab ,但是因为无法保证 s 串右侧以外也有对应的 ac 串,所以 ac 串这一部分只能含泪舍弃。相应地,db 也需要舍弃。也就是说,最终能产生贡献的部分仅为 cd ,对应 i 周围的 ef 部分。自然地,如果 ab 在对称半径内,显然整个 ab 都可以对 i 产生贡献。

以上语段可以自然地变为 \(f_i=min(f_{id+id-i},l-i)\)

有些不一样地,此处的 id 指原 id 的下标,而 l 指原 s 串最右位置的下标

两个式子都比较巧妙,对于 \(id+id-i\) ,不难发现,如果把两倍的 id 分别向左、向右展开,再砍去 i ,剩下的刚好是 j 的下标,对于 \(l-i\) ,我们发现这部分与所求部分是相对应的,所以不必纠结它到底在哪里。

注意到,上文中仅是说“无法保证 s 串右侧以外也有对应的 ac 串”,并未说答案已经统计完毕。事实上,新的回文串的回文半径很可能更长,所以还需要继续暴力向两边拓。

返回来,前面说“假设 s 能够覆盖到 i 点”,而经过上文的讲解,相信大家已经想到,如果没有覆盖到 i 点,就意味着对称点的所有内容必须全部舍弃,而先将答案初始化为 1 再完全采用暴力拓展的方式。

特别地,建议将最右端与 i 点重合也算为没有覆盖到,因为这样可以少取一个 min。

复杂度 \(\Theta(n)\) ,因为不管这样暴力那样暴力,我们保证了 s 串最右端单调向右移动,所以是 \(\Theta(n)\) 的。

细节以及答案计算

对称中心的保证

注意到,一个回文串 s 有且仅有一个对称中心当且仅当该串长度为奇数,而回文串可能为偶数。

为了解决这个问题,我们采用在字符之间(包括开头之前和结尾之后)添加无关(即不影响回文判定)字符(如“#”“$”“%”),不难证明,因为是相间放置,所以对称时,字母数与无关字符数中必然有一个为奇数,另一个为偶数,串的长度也就一定是奇数,满足了仅有一个对称中心(值得注意,本来就为奇数的回文串以字母为对称中心,偶数则以无关字符为对称中心,添加无关字符不会影响答案)。

防止越界

注意到,在拓到最边缘的两个无关字符以后,暴力程序还会继续尝试拓展,进而导致越界。

所以,我们需要在回文串的左右无关字符以外放置两个不匹配的字符,然而实际操作中只需放置一个,因为另一个不放肯定不和这个相等。

值得注意的是,这些字符不可以与无关字符相同

答案统计

众所周知,如果将奇数 n 分成相差 1 的两份 a 和 b (\(a>b\)),那么 \(\left\lfloor\dfrac{n}{2}\right\rfloor=b\) (证明过于简单,这里不予讲解)。

而无关字符一定是对应的,所以在暴力拓展过程中一定会将两端全部拓成无关字符,又因为无关字符与字母相间放置,易得 \(\left\lfloor\dfrac{求出的回文串半径长度}{2}\right\rfloor=实际回文串半径长度\)

实际上,回文串半径长度就是以该点为对称中心的回文串数 (证明过于简单,这里不予讲解)。

代码

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<cmath>
#include<iomanip>
#include<cctype>
#include<vector>
#include<stack>
#include<queue>
#include<map>
#include<set>
#include<algorithm>
#include<utility>
#include<deque>
#include<ctime> 
#include<sstream>
#include<list>
#include<bitset> 
using namespace std; 
char given_string[100233];//给定字符串 
char useful_string[300333];//所使用的字符串 
int Radius[300333];//回文半径 
inline int Initialization(){
	int 
	Length=strlen(given_string),//原串长度 
	cnt=0;//用于标记 
	useful_string[cnt++]='&',//防止越界 
	useful_string[cnt++]='#';//先标记第一个 
	for(register int i=0;i<Length;++i){
		useful_string[cnt++]=given_string[i];
		useful_string[cnt++]='#';
	}//在每次循环中,先存储该字符,再标记# 
	useful_string[cnt]='\0';//仍然是防止越界 
	return cnt;//返回串的长度 
}//预处理 
inline int Manacher(){
	int 
	ans=0,//最终答案 
	Length=Initialization(),//所使用的串的长度 
	right_s=0,//最右端 
	id=0;//对应的对称中心 
	for(register int i=1;i<Length;++i){
		if(i<right_s)//上文讲过了 
			Radius[i]=min(Radius[id+(id-i)],right_s-i);
		else//同理,不过是初始化 
			Radius[i]=1;
		while(useful_string[i+Radius[i]]==useful_string[i-Radius[i]])
			Radius[i]++;//暴力拓展 
		if(i+Radius[i]>right_s)
			right_s=i+Radius[i],
			id=i;//更新id 
		ans+=(Radius[i]>>1);//上文讲过 
	}
	return ans;
}
int main(){
	cin>>given_string;
	printf("%d\n",Manacher());
	return 0;
}
posted @ 2021-12-12 20:13  Binaries  阅读(65)  评论(0编辑  收藏  举报
浏览器标题切换
浏览器标题切换end