Manacher's Algorithm 马拉车算法

相关链接:https://blog.csdn.net/knowledgebao/article/details/84986637


这个马拉车算法Manacher‘s Algorithm是用来查找一个字符串的最长回文子串的线性方法,由一个叫Manacher的人在1975年发明的,这个方法的最大贡献是在于将时间复杂度提升到了线性,这是非常了不起的。对于回文串想必大家都不陌生,就是正读反读都一样的字符串,比如 "bob", "level", "noon" 等等,那么如何在一个字符串中找出最长回文子串呢,可以以每一个字符为中心,向两边寻找回文子串,在遍历完整个数组后,就可以找到最长的回文子串。但是这个方法的时间复杂度为O(n*n),并不是很高效,下面我们来看时间复杂度为O(n)的马拉车算法。

由于回文串的长度可奇可偶,比如"bob"是奇数形式的回文,"noon"就是偶数形式的回文,马拉车算法的第一步是预处理,做法是在每一个字符的左右都加上一个特殊字符,比如加上'#',那么

  • bob    -->    #b#o#b#
  • noon    -->    #n#o#o#n# 

这样做的好处是不论原字符串是奇数还是偶数个,处理之后得到的字符串的个数都是奇数个,这样就不用分情况讨论了,而可以一起搞定。接下来我们还需要和处理后的字符串t等长的数组p,其中p[i]表示以t[i]字符为中心的回文子串的半径(因为处理后的回文串肯定是奇数,所以半径是回文串长度/2+1),若p[i] = 1,则该回文子串就是t[i]本身,那么我们来看一个简单的例子:

#1#2#2#1#2#2#
1212521612321

为啥我们关心回文子串的半径呢?看上面那个例子,以中间的 '1' 为中心的回文子串 "#2#2#1#2#2#" 的半径是6,而未添加井号的回文子串为 "22122",长度是5,为半径减1。这是个普遍的规律么?

我们再看看之前的那个 "#b#o#b#",我们很容易看出来以中间的 'o' 为中心的回文串的半径是4,而 "bob"的长度是3,符合规律。再来看偶数个的情况"noon",添加井号后的回文串为 "#n#o#o#n#",以最中间的 '#' 为中心的回文串的半径是5,而 "noon" 的长度是4,完美符合规律。所以我们只要找到了最大的半径,就知道最长的回文子串的字符个数了。只知道长度无法确定子串,我们还需要知道子串的起始位置。   

我们还是先来看中间的 '1' 在字符串 "#1#2#2#1#2#2#" 中的位置是7,而半径是6,貌似7-6=1,刚好就是回文子串 "22122" 在原串 "122122" 中的起始位置1。那么我们再来验证下 "bob","o" 在 "#b#o#b#" 中的位置是3,但是半径是4,这一减成负的了,肯定不对。所以我们应该至少把中心位置向后移动一位,才能为0啊,那么我们就需要在前面增加一个字符,这个字符不能是井号,也不能是s中可能出现的字符,所以我们暂且就用美元号吧,毕竟是博主最爱的东西嘛。这样都不相同的话就不会改变p值了,那么末尾要不要对应的也添加呢,其实不用的,不用加的原因是字符串的结尾标识为'\0',等于默认加过了。那此时 "o" 在 "$#b#o#b#" 中的位置是4,半径是4,一减就是0了,貌似没啥问题。我们再来验证一下那个数字串,中间的 '1' 在字符串 "$#1#2#2#1#2#2#" 中的位置是8,而半径是6,这一减就是2了,而我们需要的1,所以我们要除以2。之前的 "bob" 因为相减已经是0了,除以2还是0,没有问题。再来验证一下 "noon",中间的 '#' 在字符串 "$#n#o#o#n#" 中的位置是5,半径也是5,相减并除以2还是0,完美。可以任意试试其他的例子,都是符合这个规律的,所以我们得出一个结论:最长子串的长度是半径减1,起始位置是中间位置减去半径再除以2。(这里博主采取加$符号方法规避原始串是偶数的情况,计算起始位置出现负数的情况,其实可以做个特殊处理,当起始位置出现负数的时候,我们直接置为0即可,无需额外添加$符号也可以)

那么下面我们就来看如何求p数组,需要新增两个辅助变量mx和id,其中id为能延伸到最右端的位置的那个回文子串的中心点位置,mx是与id对应的最右端的位置(这里不是最长子串,而是延伸到最右边的字段),这个算法最核心的一行代码如下:

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

可以这么说,这行要是理解了,那么马拉车算法基本上就没啥问题了,那么这一行代码拆开来看就是

if(mx <= i)
    p[i] = 1;
else{
    if(p[2*id-i] < mx-i)
       p[i] = p[2*id-i];
    else
      p[i] = mx-i;
}

如果mx<=i,因为最右端都小于等于i,所以当前p[i]的回文串只能是S[i]本身,长度只能是1。

if(mx > i)这种情况,设j=2*id-i(j是i关于id的对称点),my是mx关于id的对称点。充分利用回文的特点,利用p[j]及mx这些前边已经计算出来的结果加快p[i]的计算。

  • 如果mx-i>p[j],如下图所示,说明以j为中心的回文串起始位置大于my,也就是说以S[j]为中心的回文子串包含在以S[id]为中心的回文子串中,由于 i 和 j 对称,所以以S[i]为中心的回文子串必然包含在以S[id]为中心的回文子串中(回文定义可得),所以必有 p[i] = p[j]。既:if(p[2*id-i] < mx-i) p[i] = p[2*id-i];

  • 当 mx - i <=P[j]的时候,以S[j]为中心的回文子串不一定完全包含于以S[id]为中心的回文子串中,但是基于对称性可知,下图中两个绿框所包围的部分是相同的,也就是说以S[i]为中心的回文子串,其向右至少会扩张到mx的位置,也就是说 P[i] = mx - i。   既:if(p[2*id-i] >= mx-i)  p[i] = mx-i;

 

参见如下实现代码:

#include <vector>
#include <iostream>
#include <string>

using namespace std;
int min(int x, int y)
{
	return x > y ? y : x;
}
string Manacher(string s) {
	// Insert '$' and '#'
	string t = "$#";
	for (int i = 0; i < s.size(); ++i) {
		t += s[i];
		t += "#";
	}
	// Process t
	vector<int> p(t.size(), 0);
	int mx = 0, id = 0, maxSubLen = 0, maxSubCenter = 0;
	for (int i = 1; i < t.size(); ++i) {
		//具体详见上边算法
		p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;
		//老实的一个一个往前后扩展对比
		while (t[i + p[i]] == t[i - p[i]])
			++p[i];//往前、往后一个一个的扩展
		if (mx < i + p[i]) {//更新mx和id
			mx = i + p[i];
			id = i;
		}
		if (maxSubLen < p[i]) {//更新最长子串内容
			maxSubLen = p[i];//记录最长子串半径
			maxSubCenter = i;//记录最长子串中心位置
		}
	}
	return s.substr((maxSubCenter - maxSubLen) / 2, maxSubLen - 1);
}

int main() {
	string s1 = "12212";
	cout << Manacher(s1) << endl;
	string s2 = "122122";
	cout << Manacher(s2) << endl;
	string s = "aaaaaaaaa";
	cout << Manacher(s) << endl;
}

遗留问题:

原来博客中,作者巧妙的使用了#和$符号,简化算法复杂度,但是是否可以不引进#和$符号,达到同样的效果呢?

  • 这里提供一种思路:基数和偶数分别处理,基数思路和上边一致,偶数的情况假设最后补一位,这样无需修改原始数据就可以达到复杂度为n的算法,此思路实现后续研究。

参考资料:

1,https://www.cnblogs.com/grandyang/p/4475985.html

 


有任何问题,请联系knowledgebao@163.com

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值