manacher算法
首先我们先来看一个问题:求出字符串的最长回文子串或者它的长度,比如:"cbabfd"的最长回文子串就是"bab"它的长度为3.
来看一个暴力的解决方法:中心拓展法。
回文子串一定是对称的,所以我们可以每次循环选择一个中心,进行左右扩展,判断字符串是否相等就可以了。
但由于存在奇数和偶数的字符串,所以我们需要从一个字符串开始扩展,或者从两个字符串中间开始扩展,总共有 n + n - 1个中心。
来看下代码的实现:
String longestPalindrome(String s) {
if (s == null || s.length() < 1) return "";
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
int len1 = expandAroundCenter(s, i, i); //从一个字符扩展
int len2 = expandAroundCenter(s, i, i + 1); //从两个字符之间扩展
int len = Math.max(len1, len2);
//根据 i 和 len 求得字符串的相应下标
if (len > end - start) {
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substring(start, end + 1);
}
int expandAroundCenter(String s, int left, int right) {
int L = left, R = right;
while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {
L--;
R++;
}
return R - L - 1;
}
时间复杂度:O(n ^ 2),两层循环,每层循环都是遍历每个字符。
空间复杂度:O(1)。
如果要降低时间复杂度,该怎么办呢? Manacher's Algorithm马拉车算法登场。
马拉车算法 Manacher‘s Algorithm 是用来查找一个字符串的最长回文子串的线性方法,由一个叫Manacher的人在1975年发明的,这个方法的最大贡献是在于将时间复杂度提升到了线性。
接下来我们开始学习这个巧妙的算法。
处理字符串:
由于字符串可能是奇数可能是偶数,而为了最终的代码统一,我们可以将其全部转换为奇数。方法就是在两个数字之间插入"#"号,插入"#"的个数用于是原字符串的长度-1。
如果原来是偶数,那么加入的"#"号是奇数;偶 + 奇 = 奇;
如果原来是奇数,那么加入的"#"号是偶数;奇 + 偶 = 奇;
就此我们就已经统一了字符串的奇偶性。
然后,为了后续中心扩展的时候不越界,我们可以在头部插入"$",尾部插入"^",(现在字符串里面没有的符号就都可以)。这样当我们扩展到头部和尾部的时候,不可能和字符串中的其他字符相同,就可以结束循环。
(找来的图是头部插入"^",尾部插入"$",只不过如同上文所说,这两个符号并没有实际意义,只要符合条件都可以)
求p[i]:
p[ i ]:以i为中心的最长回文子串的长度
接下来就是算法的关键了,充分利用了回文串的对称性,也是马拉车能在线性时间内得到答案的原因。
我们用center表示当前最右边界的的对称中心,用max_right表示当前达到的最右边界,而 i 就是当前遍历到的点。
我们现在要求p[i],如果是用中心扩展法(暴力法),那就向两边扩展对比就可以了,但马拉车算法利用了回文字符串的对称性。
来看看下面几种情况,大概就明白为什么说利用了回文字符串的对称性了。
先预告一下,关键公式 : p[i] = min (max_right - i,p[2 * center - i])
情况 1.0: i >= max_right
当出现这种情况的时候,由于当前 i 的回文子串和之前的已经判断的最长回文子串没有关系,所以无法利用回文子串的对称性,需要继续暴力中心扩展。
(因为回文子串是判断 i 的两边的字符,所以即使 i == max_right 时,也对判断无帮助)
情况 2.0: i < max_right
当前,我们已经得到了p[0...i-1]。那么如果 i 在max_right的左边就可以利用我们已经计算出来的p[ j ]来计算 p[ i ];
补充一下,j 是 i 关于当前center的对称点。
这时又要分为两种情况
情况 2.1: i + p[j] <= max_right
此时 p[ i ] = p[ j ];
为什么呢?
因为以 center为中心的最长回文子串,包括了以 j 为对称中心的最长回文子串 和 i,而且 i 和 j 以center为中心对称,那么一定有p[ i ] = p [ j ]
情况2.2: i + p[ j ] > max_right
在这种情况下,只能让p[ i ] = max_right - i 了,因为我们只能保证半径到max_right这个位置是可以回文的,但是超过右边的部分我们不能判断,只能继续用中心扩展方法(暴力法)来得到最长的回文子串。
总结一下:
- i >= max_right; 直接继续暴力
- i < max_right 且 i + p[ j ] <= max_right; p[ i ] = p[ j ]
- i < max_right 且 i + p[ j ] > max_right; p [ i ] = max_right - i,然后继续暴力
考虑了,i 在center的最长子串里面,和被center的最长子串全包围和 i + p[ j] 超过了max_right, 你是否会想如果 i - p[j] 还超过了左边max_right的对称点怎么办?
其实并不会有这种情况,因为 i 的对称点 j 的最长子串不会超过 max_right,如果超过了的话,此时的最长子串应该是 j 为中心,半径为 j + p[ j ],所以根本不可能鸭。
情况看起来很多,只不过所有的所有,转换为代码,只有短短的几行:
int j = 2 * center - i;//得到i关于中心点的对称点
if(max_right > i)//在最右边界的覆盖范围内,就利用回文字符串的特性
p[i] = min(max_right - i,p[j]);//min的第一个对应i + p[j] > max_right,第二个对应 i + p[j] <= max_right
else
p[i] = 0;
顺便看下待会要进行的暴力代码
//暴力向两边扩展
while(t[i - 1 - p[i]] == t[i + 1 + p[i]])
p[i]++;
马拉车算法的优化就在于,直接省略了一些点的部分回文子串的判断,这样暴力就不用太暴力了。
比如,当 i + p[ j ] < max_right 的时候p[ i ] = p[ j ],上面这个循环直接就退出了。
更新center 和 max_right:
当我们一步一步求p[ i ]的时候,如果p[ i ]的右边界大于当前的max_right的时候,就需要更新center 和 max_right了。因为我们希望 i 尽可能的在max_right里面,所以就要更新啊,直接看代码吧。
if(i + p[i] > max_right){
//更新最右边界,以及对称中心
center = i;
max_right = i + p[i];
}
得到结果:
好了,工作结束了,来看下利用我们求的p数组,如何得到正确的解吧。
从图中可以看出,p数组里存储的数字(也就是从中心扩展的最大个数),刚好就是它去掉"#"的原字符串的总长度,比如上图中的p[6] == 5,所以他是从左边扩展5个字符,相应的右边也是扩展5个字符,也就是"#c#b#c#b#c#"。而去掉"#"恢复后就是 cbcbc。(这个叫马拉松的人的观察力真好)。
所以说,我们遍历找到p[i]的最大值,就是最长子序列了。
代码如下:
int max_len = 0;
for(int i = 0; i < len;i++){
if(max_len < p[i])
max_len = p[i];
}
如果求长度的话,到此就可以了,那如果是求那个最长的回文子串呢?
也不难,我们只要求对应在原字符串下标,再substr出来就可以了。
来看看如何求原字符串下标吧。
用p 的下标 i 减 p[i] 再除以2,就是原字符串的开头下标了。
例如我们找到p[ i ]的最大值为5,也就是回文串的最大长度为5,在T中对应下标是6,所以原字符串的开头下标是(6 - 5) / 2 = 0,返回 0 到 第 (5 - 0)位就可以了。
int max_len = 0;
int centerIndex = 0;
for(int i = 0; i < len;i++){
if(max_len < p[i]){
max_len = p[i];
centerIndex = i;
}
}
int start = (centerIndex - max_len) / 2;//最长回文子串的起点
for(int i = start; i < max_len;i++)
printf("%c",s[i]);
来分析下它O(n)的时间复杂度吧。不严谨的想一下,for里面套了一层while循环,但是很多点可以直接利用对称得到自己的解,不会进入while循环。
如果有错误,麻烦指出,以上是我今天学manaer的总结。