Manacher 学习笔记

\(\text{Manacher 学习笔记}\)

一、引入

首先我们需要知道的是 \(\text{Manacher}\) 是解决回文串问题的有效工具。

一个通用的问题模型是给定一个长度为 \(n\) 的字符串 \(s\),统计该字符串中所有的回文子串的个数。\(\text{Manacher}\) 算法可以在 \(O(n)\) 的时间复杂度内解决这个问题。

我们知道的是,对于任意一个回文串 \(s[i,j]\),有对称中心 \(\lfloor \dfrac{i+j-1}{2}\rfloor\)。考虑到奇数和偶数长度回文串的差异,我们用两个数组 \(d_1(i),d_2(i)\) 分别表示长度为奇数、偶数的回文串中心为 \(i\) 时回文串的个数。

现在我们的问题就是快速求出 \(d_1,d_2\) 两个数组。

二、算法

我们先考虑 \(d_1\) 的求解过程,求出 \(d_1\) 数组的基本思想是采用之前维护过的 \(d_1\) 数组信息来更新现有的 \(d_1\) 数组。为了叙述方便,下文以 \(d\) 数组代指 \(d_1\) 数组。

既然这样,在求 \(d(i)\) 时我们需要维护 \([1,i-1]\) 范围内边界最靠右的回文串的边界 \(l,r\),初始设 \(l=1,r=0\)

现在考虑计算 \(d(i)\) 的方式:

  • \(i>r\),暴力匹配;
  • \(i\le r\)

由于 \([l,r]\) 是回文串,那么 \(d(i)=d(l+r-i)\),也就是将中心为 \(l+r-i\) 为中心的回文串 copy 到了 \(i\) 上。然而需要留意的是,我们只能保证在 \([l,r]\) 内的部分是回文串,而这个部份内回文串的最大长度显然是 \(r-i+1\)

考虑复杂度分析:\(r\) 是单调递增的,而暴力算法运行一次一定会使得 \(r\) 增长 \(1\),因此复杂度为 \(O(n)\)

现在考虑 \(d_2(i)\) 的求解——但其实我们有更好的方法用一套模板解决两个问题。

考虑在原字符串每个字符的两侧都插入一个其它字符,这样一来所有回文串都可以归约到奇数串的情况,例如 aba \(\to\) a#b#aabba \(\to\) a#b#b#a。对于多统计的情形,容易得到关系 \(d(i)=2\times d_1(i)=2\times d_2(i)+1\)

给出代码:

void manacher() {
	int l = 1, r = 0;
	for (int i = 1; i <= n; i++) {
		int k = (i > r) ? 0 : min(d[l + r - i], r - i + 1);
		while (i + k <= n && i - k >= 1 && equ(s[i - k], s[i + k])) ++k;
		d[i] = k--;
		if (i + k > r) l = i - k, r = i + k;
	}
}

三、应用

应用典型是求最长回文子串。对于长度为奇数的回文串,最长的长度是 \(2\times d_1-1=d-1\),偶数是 \(2\times d_2=d-1\)。于是返回 \(\max_d-1\) 即可。

posted @ 2025-01-04 18:13  长安19路  阅读(29)  评论(0)    收藏  举报