最长回文子串问题
前言:2021.1.23,学校网课放假三天,于是就有了这篇。
关于回文串:
对于一个字符串 s ,如果 s = rev(s) ,就称 s 是回文串。其中 rev(s) 表示字符串 s 的翻转。好比“LeannaeL”(不要问我这串是啥意思) 。
关于回文子串:
回文串 s 的子串 t 如果是回文串,我们就称 t 是 s 的回文子串。
那么现有如下问题:给出一个字符串 s ,求出 s 最长的回文子串长度。
首先我们先用朴素算法(暴力)解决一下这个问题。
1.暴力算法:
我们可以不从字符串本身去考虑,最近本人学排列组合有种插孔的问题,所以不妨从字符与字符之间的空隙入手。
那么解决最长回文子串问题,最为朴素的算法就是解决最长回文子串问题,最为朴素的算法就是依次以每个字符或者两个字符之间的空隙为中心,当两侧对应字符相同时向两侧进行扩展,扩展到不能再扩展时,我们就得到了该中心对应的回文子串最大长度。
代码实现也就非常简单了👇
int work(char* s, int n) {
int ans = 0;
for (int i = 1; i <= n; i++) {
int l = i, r = i;
while (l - 1 != 0 && r + 1 <= n && s[l - 1] == s[r + 1])
l--, r++;
ans = max(ans, r - l + 1);
if (i < n && s[i] == s[i + 1]) {
l = i, r = i + 1;
while (l - 1 != 0 && r + 1 <= n && s[l - 1] == s[r + 1])
l--, r++;
ans = max(ans, r - l + 1);
}
}
return ans;
}
对于每个枚举的回文中心,最坏情况下要向外扩展 O(n) 次,而回文中心有 O(n) 个,因此总复杂度为 O(n²) 。
2.算法描述
Manacher
这东西叫马拉车算法(和🐎拉🚗没啥关系,音译叭),介绍它的原因是因为该算法可以在O(n)的时间复杂度求出一长为 n 的字符串的最长回文字串。
通过上述的暴力过程,我们不难发现,在求回文串的时候还是要考虑奇偶的。因此我们要做一些简单的处理,可以实现我们不再分奇偶讨论。
处理方法👇
在每个相邻字符之间都插入一个分隔符(我又想起了最近学的插孔问题了QAQ ) ,当然千万别忘了在字符串的首和尾都要添加!而且你添加的字符,必须是原串中没出现过的自负(否则…)。一般来说加个“#”。
举几个例子
原串:abaab(偶数串)
新串:#a#b#a#a#b#(变成奇数串了)
原理比较简单:设串长为 n ,则总共(算上首尾)出 n + 1 个空要填上分隔符,那么新串的总长度是 2n + 1,自然是奇数啦。
算法本身
在Manacher算法中,我们用一个辅助数组 p 记录以每个字符为中心的最长回文半径,也就是 p[i] 记录以 t[i] 为中心的最长回文子串的半径。显然, p[i] 最小为 1 , p[i] 为 1 时对应的回文子串就是字符 t[i] 本身。
我们对于上面举到的例子可以写出对应的 p 数组:
原串 | abaaba |
---|---|
新串 | #a#b#a#a#b# |
p数组 | 1 2 1 4 1 2 5 2 1 2 1 |
3.算法正确性的证明
设L为回文子串的长度,那么
L = 2 ∗ p[i] − 1 是新串 t 中以 t[i] 为中心的最长回文子串长度。
而且最重要的一点是以 t[i] 为中心的最长回文子串一定是以#(分隔符)开头和结尾的!那 L 减去最前面或者最后面的字符 # 之后就是要求的回文串长的2倍,即 (L−1) / 2 = p[i] − 1 。
问题来了 p数组咋求啊…啊这
4.代码实现
**动态规划思想:**我们依次求 p[1] , p[2] , p[3]…p[n] 的值,在求 p[i] 时,之前的 p[ ] 值已经得到了,我们就可以利用回文串的性质和之前的p[ ] 值来对求解 p[i] 的过程进行优化。
先给出求解 p 数组的核心代码,为了防止在扩展回文串的过程中越界,我们在 t 两侧分别添上了字符 + 和 - 。
for (int i = 1; i <= m; i++)
{
p[i] = 1;
if (maxid > i)
p[i] = min(p[2 * id - i], maxid - i);
while (t[i - p[i]] == t[i + p[i]])
p[i]++;
if (i + p[i] > maxid)
id = i, maxid = i + p[i];
}
while(t[i-p[i]] == t[i+p[i]])
p[i]++;
上面这段代码就是暴力的向两侧扩展回文子串。
再看👇
if (maxid > i)
p[i] = min(p[2 * id - i], maxid - i);
事实上, maxid 记录的是在求解 p[i] 之前,我们找到的回文串中,延伸到最右端的回文串的右端点位置,而 id 记录的就是这个回文串的中心的下标。
这是利用了字符串本身天然的对称性。
j = 2 ∗ id − i 就是 i 关于 id 的对称点,根据回文串的对称性, p[j] 代表的回文串时可以对称到 i 这边的,但是如果 p[j] 代表的回文串对称过来以后超过 maxid 的话,超出的部分就不能对称过来了(因为不能保证左侧超出的部分和右侧超出的部分相同),所以 p[i] 的下限就取 p[2∗id−i] 和 maxid − i 的较小者,即 :
if(maxid > i)
p[i] = min(p[2 * id - i], maxid - i);
有了一个下限之后,再暴力的扩展 p[i] 。这样做的时间复杂度是怎样的呢?我们只需要关注暴力扩展 p[i] 的复杂度。
需要暴力扩展 p[i] ,就意味着在暴力扩展前, p[i] 代表的回文串右端点就已经到了 maxid 了,那么我们每暴力扩展一次,就会使得 maxid 增加 1 ,而 maxid 最多为 m ,因此在整个过程中最多只会暴力扩展 O(m) 次,其中 m 表示新串 t 的长度。
5.结语
春有百花秋望月,夏有晾风冬听雪。心中若无烦恼事,便是人生好时节。愿你:晨有清逸,暮有闲悠,梦随心动,心随梦圆,愿你天天快乐,心更甜!感谢大家观看~