Manacher(马拉车)算法—简略讲解

这是一篇菜鸡的算法小笔记!希望你喜欢!

前言

马拉车算法是用来查找一个字符串的最长回文子串的线性方法,是一个叫 Manacher 的人在 1975 年发明的,这个方法的最大贡献是在于将时间复杂度提升到了线性,这是非常了不起的!

预处理

这里就不赘述什么是回文串了!我相信你知道的!
由于回文串的长度可以是奇数也可以是偶数!要预处理一下,统一变成奇数,方法是在每一个字符的左右都加上一个特殊字符,比如加上 ‘#’。该字符是原字符串没出现过的。
例如:
fevef --> #f#e#v#e#f#
aba --> #a#b#a#
预处理后无论原字符串长度是奇数还是偶数,得到的字符串的长度都是奇数。这样就不用分情况讨论了!统一处理!
一般为防止越界,我们都会字符串的最前面和最后面再各加上一个其他的特殊字符,比如’$’ 、’^’。

回文子串的半径

我们需要一个和预处理后所得到的字符串 s 长度大小的辅助数组 p 来记录回文子串的半径!
p[i] 表示 s[i] 为中心的最长回文子串的半径。
例子:
# f # e # v # e # f #
1 2 1 2 1 6 1 2 1 2 1
然后你会惊奇地发现,p[i]-1就是以 s[i] 为中心的最长回文子串在原字符串的长度。
比如这里的p[5]=6,中心字符是v,整个最长回文串的长度是2*p[5]-1,因为每个原生字符的两边都有特殊字符,所以在回文子串中特殊字符的个数肯定比原生字符的个数多一个,即特殊字符的个数为p[5],原生字符的个数为p[5]-1。所以该回文子串在原字符串中的长度就为p[5]-1。这是个普遍的规律!

在求出所有的p[i]后,我们的问题就转化成了:求以最大 p[i] 所对应的 s[i] 为中心的最长回文子串!

求出记录回文子串半径的辅助数组p

朴素的计算方法是以一个字符为中心,向两边拓展,直到两边的字符不相同!但这不是我们需要的!

我们有一个更好的方法!这个求p数组的方法感觉跟KMP算法求next数组的方法是有相通之处的。不需要一个一个地去匹配,而是利用前面已知的信息来更好地求出辅助数组。
我们需要两个辅助变量:mx 和 id

mx 表示 已遍历所得到的回文子串中能延伸到的最右端的位置
id 表示 最右端能延伸到mx位置的回文子串的中心位置

因为 mx 是我们已知的能延伸到的最右端的位置,而我们是要利用前面已知的信息的并且 mx 位置之前的信息,我们都是可以利用的。那么我们就要尽可能去最大化 mx。

注意:下标为 mx 的字符并不包含在回文子串里

感觉可能有点绕,举个例子说明一下!
例子:
# f # e # v # e # f #
1 2 1 2 1 6 1 2 1 2 1
mx初始化为 -1,id也初始化为 -1。

遍历过程中,出现mx > i 时,如下图所示:
在这里插入图片描述

因为回文串是左右两边对称的,所以在求 p[i] 的时候我们就可以利用这一性质更快的求得结果。这里的 id 和 mx 都应该的已知的,也就是利用了前面已知的信息,实现优化!

精髓代码:

if (mx > i) p[i] = min(p[2 * id - i], mx - i);
else p[i] = 1;

简单来说就是 mx > i 时,以 i关于id的对称点 j 为中心位置的最长回文子串被包含在 mx 的对称点到 id 这个区间时 ,那么根据对称的性质,肯定 p[i] >=p[2*id-i] 的。如果超出了这个区间,那么我们只能明确直到区间内的,区间外的我们并不能明确知道,则 p[i] >= mx - i 的。然后就可以再用朴素的方法去向左右两边拓展。

while (s[i - p[i]] == s[i + p[i]]) {//朴素的匹配方法,向左右两边扩展 
		p[i]++;
}

当 mx <= i 时,我们已有的信息并不能帮到什么,直接 p[i] = 1 (因为单个字符本身就是回文) 然后就只能乖乖的使用朴素的匹配方法。

具体实现代码:

#include<iostream>
#include<string>
#include<algorithm>
using namespace std;

string str;    //原字符串
string ans_str;//最长回文字符串

string deal(string s) {//预处理     //abbbafhksksikiaaa
	string str = "$";//加上一个原有字符串中不存在的字符,防止越界
	for (int i = 0; i < s.length(); i++) {
		str += '#';
		str += s[i];
	}
	str += '#';
	str += '^';//加上一个原有字符串中不存在的字符,防止越界
	return str;
}
void Manacher(string str) {
	string s=deal(str);
	 
	int* p = new int[s.length()];
	p[0] = 1;
	p[s.length() - 1] = 1;
	int max_pos = -1;//最长回文子串的中心位置
	int max_len = -1;//最长回文子串的长度

	int mx = 0;//已遍历所得到的回文子串能延伸到的最右端的位置,注意该索引下的字符并不包含在回文子串里
	int id=-1;//最右端能延伸到mx位置的回文子串的中心位置
	
	for (int i = 1; i < s.length()-1; i++) {
		if (mx > i) p[i] = min(p[2 * id - i], mx - i);//i在已有条件下所能延伸到的最右端的位置的左边,可由对称的性质求出
		else p[i] = 1;//i不在已有条件下所能延伸到的最右端的位置的左边,因为单个字符自身也是回文的,置为1,再使用朴素的匹配方法求出
		while (s[i - p[i]] == s[i + p[i]]) {//朴素的匹配方法,向左右两边扩展 
			p[i]++;
		}
		if (mx < i + p[i]) {//更新在已有条件下所能延伸到的最右端的位置和延伸到mx位置的回文子串的中心位置
			id = i;
			mx = i + p[i];
		}
		if (max_len < p[i]-1) {//更新最长回文子串的中心位置和最长回文子串的长度
			max_pos = i;
			max_len = p[i] - 1;
		}
	}
	ans_str = str.substr((max_pos - p[max_pos]) / 2, max_len);//最长回文子串
}
int main() {
	while (cin >> str) {
		Manacher(str);
		cout << "最长回文子串:" << ans_str << endl;
	}
	return 0;
}

PS:希望对你有帮助!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值