如何高效的统计最长回文子串的长度? Manacher马拉车算法通俗讲解与实践教程

待解决问题描述

给定一个字符串abcbddacaddx要求输出这个字符串的最长回文子串的长度。

解释:子串是指从这个字符串任意地方截取一段任意长度的字符串。回文子串指的是前面所提到的截取出的子串中是对称的那种子串。比如“bcb”就是对称。

如果我们是要求出最长回文子串那么有一个简单的方式就是将原字符串逆序,然后匹配原字符串与逆序后的字符串之间最长公共子串。但是现在我们只需要统计最长回文子串的长度。所以我们不需要知道具体是哪段字符串。只需要知道长度。我们需要计算的信息量大大降低,这就意味着有更快速的解决方案。一般是用Manacher算法。

Manacher算法的思想(马拉车算法)

注意:我们只需要求得一个长度值。是最长回文子串的长度值。不需要求具体是哪段子串。了解这个信息可以帮助我们理解马拉车算法是如何节省计算量的。

马拉车算法的思想非常简单。它就是从左往右遍历计算出以当前元素为中心点所能向周围拓展的回文子串最大半径。比如“abcb”中的"c"的最大拓展半径是2.仔细一想其实“以当前元素为中心点所能向周围拓展的回文子串”这句话是有一点点问题的。比如:“bccb”中的第一个“c”似乎以他为中心拓展的回文串并不是关于它对称的,本质的原因就是因为这个回文子串的字符个数是偶数。只有当回文子串中的字符个数是奇数才会关于中心元素对称。为何将回文子串的字符个数变成奇。Manacher就想了一个办法将每个元素中间插入一个原字符串中不存的字符。比如“#”。

举个例子:

  • “bccb”各个字符中间插入一个#就变成了“#b#c#c#b#”,可以看到原先的偶数长度回文串变成了奇数长度回文串。
  • “bcb” 各个字符中间插入一个“#”就变成了“#b#c#b#”,可以看到原先奇数长度的回文串仍然还是奇数长度回文串。

前面介绍的是一些预处理。现在开始介绍马拉车算法的核心思想。它的本质在于重复利用之前已经算出的那些字符的最大拓展长度。

注意:它是从左往右遍历计算出当前字符的最大拓展长度。这意味着当它遍历到某个字符时,它左边的那些字符的最大拓展长度已经计算出来了,它右边的那些字符最大拓展长度并没有计算出。马拉车算法就是充分利用了左边已经计算出的那些字符的最大拓展长度来节省计算量的

马拉车算法在计算某个字符的最大拓展长度时候已知两个量:

  1. 当前字符左边那些各个字符的最大拓展长度
  2. 左边那些字符为中心的回文子串中,能到达最右边的那个回文串的中心字符下标

根据“能到达最右边的那个回文串的中心字符下标”就能知道当前字符是否属于“能到达最右边的那个回文串”的一部分。如果是的话根据对称性就能够知道当前字符串的最少能够拓展的半径。

记各个字符最大拓展长度的数组为len[]。能到达最右边的那个回文串的中心字符下标记为far_index.当前字符的下标为i。
因此能到达最右边的那个回文串的最右端的那个字符下标为:far_index+len[far_index]-1。如果far_index+len[far_index]-1i大证明当前字符是属于能到达最右边的那个回文串的一部分。当前字符的最少拓展长度是可以根据它的对称元素的最大拓展长度算出来。当前字符下标是i,因此它关于far_index的左边那个对称点的下标为:2*far_index-i。这个字符的最大拓展长度是已知的。根据对称性当前字符最小拓展长度是min(len[2*far_index-i], far_index+len[far_index]-i)。然后当前元素最大拓展长度那就得根据这个最小拓展长度继续往外拓展统计最大长度。

最终的我们可以知道各个回文子串的长度。注意这个字符串中间是夹杂了“#”的。我们得到最终的最长回文字符串长度是需要去掉这个“#”。

比如第i个元素最大拓展长度是len[i]。如果它是“#”那么它对应回文子串长度为(len[i]-1).如果不是“#”那么它对应的回文子串长度为(len[i]-1)*2+1.
Manacher马拉车算法的c++代码如下所示:

string s = "#d#a#a#";
	vector<int> len(s.size(), 1);
	int far_index = 0;
	int max_index =0;
	int max_len = 0;
	for (int i = 0; i < s.size(); i++)
	{
		if (far_index + len[far_index] > i)
		{
			len[i] = min(len[2 * far_index - i], far_index + len[far_index]-i);
		}
		while (i - len[i]>=0 && i + len[i]<s.size() && s[i + len[i]]==s[i-len[i]])
			len[i]++;

		if (len[i] > max_len)
		{
			max_len = len[i];
			max_index = i;
		}
	}

	if (s[max_index] != '#')
		cout << (max_len-1)/2*2+1;
	else
		cout << max_len - 1;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章