manacher(马拉车)算法求最长回文子串

本文深入讲解Manacher算法,一种高效求解最长回文子串问题的算法,通过预处理、计算最长回文半径等步骤,实现O(n)时间复杂度。文章详细解释了算法原理,并提供了C++代码示例。
问题提出

:给定一个字符串,要求其最长回文子串
例如
“aaaa”,其最长回文子串为4,即“aaaa”,
“abcd”,最长回文子串为1,
“abccb”,最长回文子串为4,即“bccb”。

如果直接对每一个字符遍历,分别向两边查找的话,时间复杂度为O( n 2 n^2 n2),效率很低。
那有没有更高效的算法呢。
1975年,一个叫Manacher的人发明了一种算法,即manacher算法,时间复杂度可以达到O( n n n)。

算法描述

1 、 1、 1预处理

由于回文串分为奇回文(长度为奇数)、和偶回文(长度为偶数),本因分两种情况处理,但为了简化这一步,我们将对原字符串进行处理,使之变成奇回文。
具体做法是在每个字符左右两边都加上一个特殊字符,如:
原字符串为,abba,长度为4,
处理后,#a#b#b#a#,长度为9。
原字符串为,aba,长度为3,
处理后,#a#b#a#,长度为7。

2 、 2、 2最长回文子串长度

我们引入一个辅助数组,int p[ ]p[i]表示以原字符串s[]i位置处s[i]为中心的最长回文半径
以字符串 abbaf 为例,预处理后字符串为 #a#b#b#a#f#

i       0  1  2  3  4  5  6  7  8  9 10  11 12
arr[i]  #  c  #  a  #  b  #  b  #  a  #  f  #
p[i]    1  2  1  2  1  2  5  2  1  2  1  2  1

在上面这个例子中,最长回文子串为abba。以s[6]为中心,其最长回文半径为p[6]=5,所覆盖的字符串是#a#b#b#a#,对应的原始字符串为abba,即最长回文子串,长度为4,可以通过5-1得到。
接下来我们来探究最长回文子串长度最长回文半径之间的关系
再看几个例子,aba, 转换之后为#a#b#a#,最长回文子串为aba,长度为3,最长回文半径为p[4]=4
再如,#a#e#f#e#a#,最长回文子串为aefea,长度为5,计算得最长回文半径为p[6]=6
我们发现,最长回文子串长度=最长回文半径-1,即p[i]-1
事实上,的确存在这个结论。我们可以这样理解,最长回文子串即以某一点i为中心,以p[i]为半径所覆盖的线段,那么对于原始字符串最长回文子串长度=p[i]*2。由于预处理后,每个字符左右都加了一个特殊字符,所以最长回文半径变为了原来的2倍,最长回文子串长度变为了原来的*2-1,那么最长回文子串长度=p[i]-1

3 、 3、 3计算p[]数组

现在知道了要求的最长回文子串长度与最大回文半径p[i]有关,那么接下来就讨论最大回文半径p[i] 怎么求。
我们设置两个变量idmxid表示能延伸到最右端的回文子串的中心位置,mx表示该子串的最右端位置。
那么有,mx=id+p[id]
在这里插入图片描述
对于i<mx,在以id为中心的回文子串中,存在另一以j为中心的回文子串,与以i为中心的回文子串关于id对称,且有p[j]=p[i]。由于对称的性质,所以有i+j=id*2j=id*2-i
考虑到i的回文子串可能存在超出mx的部分,所以此时p[i]=mx-i,依然有p[j]=p[i]
至于超出mx的部分是否满足上述条件,就需要遍历去比较字符了。
所以,在i<mx的情况下,有p[i]=min(p[id*2-i],mx-i)
当满足mx<i+p[i]时,更新mx=i+p[i]id=i

4 、 4、 4参考代码 C++

char s[maxn];//原始字符串
char s1[maxn];//预处理后字符串
char p[maxn];//最长回文半径
int init()//预处理
{
	int len=strlen(s);
	s1[0]='$';//处理边界,防止越界
	s1[1]='#';
	int j=2;
	for(int i=0;i<len;i++)
	{
		s1[j++]=s[i];
		s1[j++]='#';
	}
	s1[j]='\0';//处理边界,防止越界
	
	return j;//返回处理后的字符串长度
}
int manacher()//计算最长回文子串长度
{
	int len=init();
	int maxlen=-1;//最长回文子串长度
	int id;//可以不初始化是因为初始循环一定会执行if(mx<i+p[i])
	int mx=0;
	for(int i=1;i<len;i++)
	{
		if(i<mx)
			p[i]=min(p[2*id-i],mx-i);
		else p[i]=1;
		while(s1[i-p[i]] == s1[i+p[i]])//遍历比较字符,因为已经处理过边界,所以无需进行边界判断
			p[i]++;
		if(mx<i+p[i])//更新id和mx
		{
			id=i;
			mx=i+p[i];
		}
		maxlen=max(maxlen,p[i]-1);
	}
	return maxlen;//返回最长回文子串长度
}

5 、 5、 5参考习题
洛谷P3805 【模板】manacher算法
or HDU3068 最长回文

<think>好的,我现在需要帮助用户理解拉车算法Manacher's Algorithm)的实现方法和原理,以及如何用它优化最长回文子串检测。用户之前已经了解过中心扩展法,现在想升级到更高效的算法。我需要参考提供的引用内容,特别是引用1、3和4中的信息,来构建一个清晰的解释。 首先,我得回忆拉车算法的核心思想。根据引用3,拉车算法通过预处理字符串和处理奇偶回文的情况,将时间复杂度降到了线性O(n)。引用4中的代码示例展示了如何插入分隔符(如#号)来统一处理奇偶长度的回文,这是关键的一步。同时,算法利用回文半径和对称性来避免重复计算,这应该和动态规划或中心扩展法中的某些优化有关。 接下来,我需要拆解算法的步骤。预处理部分,插入特殊字符将字符串转换为奇数长度,这样可以统一处理奇偶情况。例如,字符串"abba"变成"#a#b#b#a#",每个字符周围都有#,这样无论原回文是奇数还是偶数长度,转换后都变成奇数长度,方便处理。 然后,维护几个关键变量:当前最大右边界R,对应的中心C,以及每个位置的回文半径数组P。当遍历每个字符时,利用对称性(mirror)来确定初始回文半径,这样可以减少不必要的扩展次数。例如,如果当前字符i在R的左侧,可以利用其关于C的对称点mirror的P值来初始化P[i],避免从零开始扩展。 在代码实现部分,需要处理不同的case,比如i在R内或外的情况,然后进行中心扩展。引用4中的Java代码展示了如何处理这些情况,包括更新C和R的位置,以及记录最大回文长度和对应的中心点。我需要将这段代码转换为C++,并确保正确性,比如处理字符串预处理和回文半径的计算。 此外,用户可能对算法的时间复杂度分析感兴趣。拉车算法的时间复杂度是O(n),因为每个字符的扩展操作总体上是线性的,每个字符最多被扩展一次。这需要详细解释,确保用户明白为什么这个算法比中心扩展法更高效。 同时,用户的问题提到了优化方法和代码示例,所以需要将理论解释与代码结合,逐步说明每个部分的作用。比如,预处理字符串的部分,如何构造新的字符串,然后初始化P数组,遍历每个字符,利用对称性确定初始半径,然后进行扩展,更新最大边界和中心。 可能还需要对比其他方法,如动态规划和中心扩展,突出拉车算法的优势,如线性时间和较低的空间复杂度(O(n))。引用2中提到的动态规划空间复杂度较高,而拉车算法在空间上只需要一个数组,因此更优。 测试用例也是重要的,需要提供几个示例来验证代码的正确性,比如"babad"应该返回"bab"或"aba",而"cbbd"应该返回"bb"。这有助于用户理解代码的实际效果。 最后,确保回答结构清晰,分步骤讲解,原理与实现并重,引用相关的参考文献,并在末尾添加相关问题,引导用户深入思考。比如,如何修改代码以返回最长回文子串本身,或者如何处理特殊字符的情况等。</think>### 拉车算法Manacher's Algorithm)详解 --- #### 一、算法核心原理 拉车算法通过**预处理字符串**和**利用回文对称性**,将时间复杂度优化到$O(n)$。其核心优化点包括: 1. **预处理插入分隔符**:将任意字符串转换为**奇数长度**,统一处理奇偶回文 - 例如:`"abba" → "#a#b#b#a#"` 2. **维护回文半径数组**:记录每个位置的最长回文半径$P[i]$ 3. **动态右边界维护**:通过当前最大右边界$R$和中心点$C$减少重复计算[^3] --- #### 二、算法步骤分解 ##### 步骤1:字符串预处理 ```cpp string preprocess(const string& s) { string t = "^#"; for (char c : s) { t += c; t += '#'; } t += "$"; // 避免边界判断 return t; } ``` *作用*:原字符串`"abc"`转换为`^#a#b#c#$`,所有回文子串长度变为奇数[^4] --- ##### 步骤2:核心算法实现 ```cpp string manacher(const string& s) { if (s.empty()) return ""; string t = preprocess(s); int n = t.size(); vector<int> P(n, 0); // 回文半径数组 int C = 0, R = 0; // 当前最大回文中心及右边界 int max_len = 0; // 最大回文长度 int center = 0; // 最长回文的中心 for (int i = 1; i < n-1; ++i) { int mirror = 2 * C - i; // 计算i的对称点 // 关键优化:利用对称性初始化P[i] if (i < R) { P[i] = min(R - i, P[mirror]); } // 中心扩展 while (t[i + P[i] + 1] == t[i - P[i] - 1]) { P[i]++; } // 更新右边界和中心 if (i + P[i] > R) { C = i; R = i + P[i]; } // 记录最大值 if (P[i] > max_len) { max_len = P[i]; center = i; } } // 计算结果在原字符串中的位置 int start = (center - max_len) / 2; return s.substr(start, max_len); } ``` --- #### 三、关键优化技术解析 | 技术 | 实现方式 | 优化效果 | |------|----------|----------| | 对称性利用 | 通过镜像点`mirror`获取初始半径 | 避免重复计算 | | 动态边界维护 | 实时更新$C$和$R$ | 限制扩展次数 | | 预处理统一处理 | 插入分隔符`#` | 消除奇偶差异 | --- #### 四、时间复杂度分析 - **预处理阶段**:$O(n)$ - **主循环阶段**:每个字符最多被扩展一次,总扩展次数不超过$n$次 → **整体$O(n)$**[^3] --- #### 五、测试用例验证 ```cpp int main() { cout << manacher("babad") << endl; // 输出 "bab" 或 "aba" cout << manacher("cbbd") << endl; // 输出 "bb" cout << manacher("a") << endl; // 输出 "a" return 0; } ``` --- #### 六、与其他算法对比 | 算法 | 时间复杂度 | 空间复杂度 | 适用场景 | |------|------------|------------|----------| | 暴力法 | $O(n^3)$ | $O(1)$ | 教学演示 | | 中心扩展 | $O(n^2)$ | $O(1)$ | 常规应用 | | 动态规划 | $O(n^2)$ | $O(n^2)$ | 子问题分析 | | **拉车算法** | $O(n)$ | $O(n)$ | 大规模数据处理[^2] | --- ### 七、算法特性总结 1. **线性时间复杂度**:适用于处理百万级长度的字符串 2. **空间效率高**:仅需一个长度$n$的辅助数组 3. **边界处理技巧**:通过添加`^`和`$`符号避免特殊判断[^4] --- ### 相关问题 1. 如何处理字符串中的特殊字符(如`#`本身)? 2. 如何修改算法同时返回所有最长回文子串? 3. 为什么预处理时选择`#`作为分隔符? 4. 如何证明该算法的时间复杂度是严格的$O(n)$?[^3]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值