Manacher 算法(马拉车)
Manacher算法,中文名叫马拉车,是解决找出最长回文子串等这些问题最快的算法,时间复杂度逼近线性。
首先要理解Manacher算法,需要先知道中心扩展法。
判题工具参考LeetCode第5题:最长回文子串
暴力寻找最长回文子串的方式就是穷举每一个子串,非常慢。一个简单的优化就是以每个字符或者相邻两个字符的中间作为可能的最长回文子串的中心,向左右两边扩展。这样我们就可以避免判断毫无意义的子串。
而Manacher算法就是在中心扩展法的基础上又添加了一些优化。
参考坑哥在b站上录的视频,非常详细,极度推荐。【算法ABC × Manim】探究字符串的对称奥秘,小学三年级都能听懂的马拉车算法
思路:
算法的核心思想:维护一个当前最右回文子串。每次你遍历到一个新的点上时,就只有两种情况:
- 当前位置在最右回文子串的右边,那么最右回文子串无法优化,老老实实使用中心扩展法。
- 当前位置在最右回文子串的里边,由于是回文串,那么你一定能在左半边找到另一半:
- 如果另一半位置上的最长回文子串的长度没有超过当前最右回文子串的左边界,那么你就可以将另一半的长度拿下过来,然后从你拿到的长度开始进行中心扩展法。
- 如果另一半位置上的最长回文子串的长度超过了当前最右回文子串的左边界,那么你只能拿到左半边为止,因为只有在当前最右回文子串的内部,才能保证左边和右边是一样的,超过部分可能你的另一半和你对应的位置是不同的。然后同样从你拿到的长度开始进行中心扩展法。
总结一下就是Manacher算法同样是中心扩展法,只不过不是每次都从起点往外扩散,通过维护一个当前最右回文子串,使其跳过可以确定是回文的部分。
问题:
问题一:
当回文串长度是奇数时可以找到中心,那如果回文串是偶数怎么找呢?
答:前面说了中心扩展法要么从当前位置向两边扩展,要么从两个相邻的位置向两边扩展,这样写比较麻烦。我们可以通过在每个字符中间加上一个#
,这个#
就是两个相邻的位置的中间,这样就解决了偶数的回文子串的问题了。
问题二:
当前位置在最右回文子串的里边的这种情况一定是在最右回文子串的中心的右边吗,为什么不可能在中心点的左边呢?
答:我们可以用反证法来论证这个问题。当前遍历到的点在当前最右回文子串的中心的左边。
更新当前最右回文子串是通过从它的中心向外扩展而计算得来的,既然如此,那么中心左边的点一定是已经遍历过的点。而当前遍历到的点并没有遍历过,因为你当前正要遍历它,所以属于还未遍历到的点。如此一来还未遍历过的点可能是已经遍历过的点这句话有矛盾。所以得出结论:当前遍历到的点不可能在当前最右回文子串的中心的左边。
代码模板
先来一份cpp的代码,写有详细注释。
#define PII pair<int, int> // first: 当前最长回文子串一半的长度,second:
const int N = 2010;
int d[N];
class Solution {
public:
int size;
PII expand(const vector<char>& arr, int cur) { // 将当前cur位置展开,计算当前最长回文子串的一半。
int ret = d[cur];
int l = cur - d[cur], r = cur + d[cur];
while (l >= 0 && r < size && arr[l] == arr[r]) --l, ++r, ++ret;
return make_pair(ret, r - 1);
}
string longestPalindrome(string s) {
vector<char> arr = {'#'};
for (auto ch : s) { // 回文串可能是偶数的,中心点可能夹在两个字符串的中间。
arr.push_back(ch);
arr.push_back('#');
}
size = arr.size();
memset(d, 0, sizeof d);
int maxlen = 0, maxidx = -1;
for (int i = 0, r = -1, p = -1; i < size; i++) { // r : 最右回文子串的右边界,p : 最右回文子串的中心位置。
PII ret;
if (i <= r) d[i] = min(d[p - i + p], r - i + 1); // 如果当前回文串在最右回文子串内那么你就找对称的另一半,但不能超过最右回文子串的边界
ret = expand(arr, i);
d[i] = ret.first;
if (r < ret.second) { // 更新最右回文子串
r = ret.second;
p = i;
}
if (maxlen < d[i]) { // 记录最长的回文子串。
maxlen = d[i];
maxidx = i;
}
}
string ans = "";
for (int i = maxidx - maxlen + 1; i < maxidx + maxlen; i++) { // (maxidx - maxlen + 1) + (maxlen * 2 - 1)
if (arr[i] != '#') ans.push_back(arr[i]);
}
return ans;
}
};
再来一份Java代码,更加的简洁,好记。
class Solution {
public String longestPalindrome(String s) {
int n = s.length(), m = n + n + 1;
int[] d = new int[m];
char[] arr = new char[m];
arr[0] = '#';
for (int i = 1; i < m; i += 2) {
arr[i] = s.charAt(i / 2);
arr[i + 1] = '#';
}
int maxlen = - 1, maxidx = -1;
for (int i = 0, p = -1, r = -1; i < m; i++) {
if (i <= r) d[i] = Math.min(d[p + p - i], r - i);
int lef = i - d[i], rig = i + d[i];
while (lef >= 0 && rig < m && arr[lef--] == arr[rig++]) ++d[i];
if (r < rig - 1) {
r = rig - 1;
p = i;
}
if (maxlen < d[i]) {
maxlen = d[i];
maxidx = i;
}
}
StringBuilder ans = new StringBuilder();
for (int i = maxidx - maxlen + 1; i < maxidx + maxlen; i++)
if (arr[i] != '#') ans.append(arr[i]);
return ans.toString();
}
}
maxlen + 1; i < maxidx + maxlen; i++)
if (arr[i] != ‘#’) ans.append(arr[i]);
return ans.toString();
}
}