回文串查找算法

前言

接下来,我们用 \(a_{l,r}\) 表示字符串 \(a\) 在下标 \(l\)\(r\) 内的所有字符组成的字串,用 \(|a|\) 表示字符串 \(a\) 的大小。

例如

\(a=^"abc^"\)

\(a_{1,2}=^"bc^", |a|=3\)

概念

回文串

若字符串 \(s\) 为回文串,则 \((\forall i\in [0, |s|-1])(s_i=s_{|s|-i-1})\)

回文串查找问题

在给定字符串 \(s\) 中查找其回文子串,即是回文串的字串。

暴力算法

算法复杂度

时间复杂度 \(O(|s|^3)\)

空间复杂度 \(O(|s|)\)

算法思想

由于要查找回文子串,则只需枚举每个子串判断是否为回文串即可。

算法流程

\(O(n)\) 枚举字符串的子串的左端点 \(l\),再 \(O(n)\) 枚举子串右端点 \(r\)

然后 \(O(n)\) 枚举判断字符串 \(s_{l,r}\) 是否为回文串。

算法伪代码

find():
n = size s
for l in [0, n - 1]:
	for r in [0, n - 1]:
		flag = true
		for i in [0, r - l]:
			if s[l + i] != s[r - i]:
				flag = false
				break
		if flag = true:
			found
end

改进算法

算法时间复杂度

时间复杂度 \(O(|s| ^ 2)\)

空间复杂度 \(O(|s|)\)

算法思想

容易发现,一个回文串同时去除左右两端的字符,还是一个回文串。

那么暴力算法在这种情况下,会重复判断多次。所以给了我们很大改进空间。

即考虑一种方法减少判断次数。

我们可以枚举回文串中心,然后尝试向两边拓展,这样是 \(O(n^2)\) 的。

算法流程

先枚举回文串对称中心 \(i\)(注意,对称中心可以在两个字符的中间,比如说 \(aa\))。

然后向两倍拓展,枚举回文串半径 \(r\)

每次尝试拓展回文串的半径,若原回文串左右两边的字符相同,则可以构成一个半径更大的回文串。

若无法继续扩大,则放弃,继续枚举下一个 \(i\)

算法伪代码

find():
n = size s
for i in [0, n - 1]:
	r = 0
	while 1:
		if (i - r < 0) or (i + r > n - 1) or (s[i - r] != s[i + r]):
			break
		found
		r++
	r = 0
	while 1:
		if (i - r < 0) or (i + r + 1 > n - 1) or (s[i - r] != s[i + r + 1]):
			break
		found
		r++
end

其中,for 循环中第一个 while 循环,查找对称中心在 \(i\) 处的回文串,第二个 while 循环,查找对称中心在 \(i\)\(i+1\) 中间的回文串。

manacher 算法

manacher 算法是用来查找一个字符串的最长回文子串的线性方法,由 Manacher 在 1975 年发明。

也称马拉车算法,wxd叫他没马拉车

算法复杂度

时间复杂度 \(O(|s|)\)

空间复杂度 \(O(|s|)\)

算法思想

改进算法已经将时间复杂度降低至 \(O(|s|^2)\), 若要继续提高,我们需要通过减少无用的判断(或者说提高判断的效率)来降低时间复杂度。

我们发现,回文子串关于对称中心两边对称,所以若一个回文子串左边完全包含一个回文子串,那么代表他右边也会有一个回文子串。

即假设回文子串 \(s_{l,r}\) 左边存在一个回文串,即存在回文子串 \(s_{l_2,r_2}\) 满足 \(l \leq l_2 \leq r_2 \leq \frac{l+r}{2}\)

那么由于 \(s_{l,r}\) 为回文串,则 \((\forall i \in [l,r])(s_i = s_{l + r - i})\)

\(s_{l_2,r_2}\) 也为回文串,则 \((\forall i \in [l_2,r_2])(s_i = s_{l_2 + r_2 - i})\)

因为 \([l_2, r_2] \in [l, r]\),所以 \((\forall i \in[l_2,r_2])(s_i = s_{l + r - i})\)

所以有 \((\forall i \in[l_2,r_2])(s_{i} = s_{l_2 + r_2 - i} = s_{l + r - l_ 2 - r_2 + i})\)

即有 \((\forall i \in [l_2, r_2])(s_{l + r - l_2 - r_2 + i} = s_{l + r - i})\)

所以 \((\forall i \in [l + r - r_2, l + r - l_2])(s_{2l + 2r - l_2 - r_2 - i} = s_{i})\)

所以 \(s_{l+r-r_2,l+r-l_2}\) 也是回文串。

注意,但如果右边回文子串的最右边是回文子串 \(s_{l,r}\) 的最右边,即 \(l + r - l_2 = r\),或者说 \(l = l_2\),此时最右边的回文串可能能更长,因为我们不知道 \(s_{l + r - l_2 + 1}\)\(s_{l + r - r_2 - 1}\) 的关系。此时我们需要暴力拓展。

算法流程

首先,我们考虑优化掉回文串中心可能存在于字符中间的情况。

我们可以在字符件添加另一种字符,比如说:\(\#\)

 1 2 3 4 5 6 7 8
#1#2#3#4#5#6#7#8#

我们定义一个数组 \(p\)\(p_i\) 代表以第 \(i\) 个字符为中心点的子回文串的半径。

我们可以通过 \(p_i\)\(i\) 来推出回文串的左右端点。

一般情况下,回文串的中点位置 \(i\) 减去 \(p_i\) 是他的起始点位置。方便起见,在此处我们定义,半径为其左部分字符的长度 + 1。因为这样, \(p_i - i\) 就是其起始点的前一个字符。

同时,为了防止越界,我们在字符串前在添加一个字符 \(@\)

我们需要注意,这里只是在添加了\(\#\) 之后的字符串,我们如果要当前位置所代表的字符在原来字符串上的位置,只需除以 \(2\) 即可。

\(p_i\) 如何推出?

我们设 \(r\) 为当前之前的回文串能到达的最右边位置 + 1(不在回文串里),\(d\) 为能到达最右边的位置的回文串的中心点。

如果 \(r > i\),即有回文串(假设为 \(s_{l,r}\),中心点为 \(d\))能够覆盖到 \(i\) 的位置, 说明以 \(i\) 为中心的一些回文串一定在 \(i\) 关于 \(d\) 对称处出现过,而且其半径一定大于等于之前出现的半径,所以 \(p_{i} = min\{p_{2d - i},r-i\}\)\(2d-i\) 即为关于 \(d\) 对称后的 \(i\))(\(r-i\) 是由于之前的回文串可能往外拓展,但拓展的部分不在 \(s_{l,r}\) 内),然后我们再尝试拓展回文串。

\(r <= i\),则代表之前回文串不能覆盖到 \(i\) 的位置,我们直接拓展回文串(在这之前,你也可以把 \(p_{i}\) 设为 \(1\))。

即:

\[p_{i} = r > i?min{P_{2d - i}, r - i} : 1 \]

然后,我们更新 \(r\)\(d\)

我们从左往右一次对每个位置做以上操作,最终能够得出 \(p\) 数组,相当于查找了所有回文串。

算法伪代码

manacher(str)
	n = size str
	s = "@#";
	for i in [0, n - 1]:
		s = s + str[i]
		s = s + '#'
	r = 0
	d = 0
	m = size s
	for i in [1, m - 1]:
		p[i] = r > i ? min{p[2 * d - i], r - i} : 1
		while s[i - p[i]] = s[i + p[i]]:
			p[i]++;
			found
		if i + p[i] > r:
			r = i + p[i];
			d = i;
		
	return 0;

回文自动机

回文自动机支持更多有关回文串的操作,查询仅仅是其部分功能。

未完待续

posted on 2023-03-29 13:44  Evan_song  阅读(214)  评论(0)    收藏  举报