[字符串入门]Manacher 算法
#0.0 问题简述
给定一个字符串,求出其最长回文子串。例如:
- \(s=“abcd”\),最长回文长度为 \(1\);
- \(s=“ababa”\),最长回文长度为 \(5\);
- \(s=“abccb”\),最长回文长度为 \(4\),即 \(bccb\)。
#1.0 朴素算法
首先注意到回文串可能有奇、偶两种不同的情况,讨论起来比较复杂,这里先只看奇回文的情况。
思路很简单:枚举每个字符作为回文串的中点,不断向左右两端扩展,直到到达边界或两边字符不同。
伪代码:
不难看出,这样的过程的时间复杂度是 \(O(n^2)\) 的。
#2.0 Manacher 算法
个人觉得,\(\texttt{Manacher}\) 算法比较适合直接看算法流程来进行学习、理解,所以直接来看算法怎么实现吧!
#2.1 初步处理
第一步,将上文提到的奇偶回文串的问题解决掉,具体方案是:
- 在字符串首尾及每个字符间都插入一个 "#",这样可以使得原先的奇偶回文都变为奇回文;
- 在首尾两端各插入 "$" 和 "^",这样中心扩展回文的时候会自动退出循环,不需每次判断是否越界。
注意:上述新插入的三个字符,即 "#"、 "$" 和 "^",必须各异,且不可以与原字符串中的字符相同。
定义 p[i]
为以 i
为中心的最长回文半径长度。
让我们来看一个例子:\(abcbaba\),处理后如下表:
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
S[i] | $ | # | a | # | b | # | c | # | b | # | a | # | b | # | a | # | ^ |
p[i] | 1 | 1 | 2 | 1 | 2 | 1 | 6 | 1 | 2 | 1 | 4 | 1 | 2 | 1 | 2 | 1 | 1 |
不难发现 p[i]-1
便是原字符串中以 S[i]
为中心的最长回文串的长度。
#2.2 求解 p 数组
接下来我们的重点就是如何求出 p[]
。这里定义几个变量:
id
表示当前位置加上该位置最长回文半径可覆盖的范围最远的位置;mx
表示id + p[id]
,即id
加上id
的最长回文半径可覆盖的最远范围的右边界;
那么我们应该如何运用 id
和 mx
来帮助求解 p[]
呢?来看下面的讨论。
如上图,假设当前枚举的 \(i < mx\) ,那么以 \(i\) 为中心的回文串就只有以下几种状态:
- 完全被 \(mx\) 的对称点 和 \(mx\) 包含;
- 超出 \(mx\) 的范围;
我们发现,位置 \(i\) 关于 \(id\) 的对称点 \(i'\) 的最长回文半径已经求出,而我们知道,\(mx\) 是以 \(id\) 为中心的最长回文半径可以覆盖到的最远距离的有边界,也就意味着 \([mx'+1,mx-1]\) 这个串必然是回文串,所以 \([mx'+1,id]\) 应当是和 \([id,mx-1]\) 这个串关于 \(id\) 对称。
所以,如上图,如果以 \(i'\) 为中心的最长回文串无法超出 \(mx'\),那么 \(i\) 的最长回文串一定也不会覆盖 \(mx\);
但如果以 \(i'\) 为中心的最长回文串的范围超出了 \(mx'\),即超出了以 \(id\) 为中心的回文串的范围,那么以 \(i\) 为中心的回文串一定不会超出 \(mx\),也就是一定不会超出以 \(id\) 为中心的回文串的范围。如上图,两端紫红色的文本串必然不是相同的,否则这与 \(mx\) 的定义相悖,于是以 \(i\) 为中心的回文串必然不会超过 \(mx.\)
但是,假如如上图,以 \(i'\) 为中心的回文串恰好与以 \(id\) 为中心的回文串左端对齐,那么以 \(i\) 为中心的回文串却是有可能向左右两端继续扩展的。
综合上面所说的几点,我们可以得到以下代码:
if (i < mx) p[i] = min(p[2 * id - i], mx - i);
else p[i] = 1;
while (s[i - p[i]] == s[i + p[i]]) p[i] ++;
没错,上面这 \(3\) 行代码就已经是 \(\texttt{Manacher}\) 算法的核心了,下附完整代码。
#2.3 代码实现
const int N = 23000010;
const int INF = 0x3fffffff;
int len, ans= -1, p[N];
char s[N];
template <typename T>
inline T Min(const T a, const T b) {
return a < b ? a : b;
}
template <typename T>
inline T Max(const T a, const T b) {
return a > b ? a : b;
}
inline void input() {
s[len ++] = '$', s[len ++] = '#';
while (cin >> s[len]) s[++ len] = '#', ++ len;
s[len ++] = '^', s[len] = '\0';
}
inline void Manacher() {
int id = 0, mx = 0;
for (int i = 0; i < len; i ++) {
if (i < mx) p[i] = Min(p[2 * id - i], mx - i);
else p[i] = 1;
while (s[i - p[i]] == s[i + p[i]]) p[i] ++;
if (mx < i + p[i]) id = i, mx = i + p[i];
ans = Max(ans, p[i] - 1);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); input();
Manacher(); cout << ans;
return 0;
}
\(\texttt{Manacher}\) 算法的时间复杂度仅为 \(O(n)\),是极为优秀的回文串算法。
参考资料
[1] Manacher 算法 - 刘毅