前言
算法,又称马拉车算法,是解决回文问题的利器,在字符串题中也非常常用。
最重要的是,它是个简单易懂的算法。
让我们从暴力开始
如何求出一个字符串中最长回文串的长度?
废话,当然是暴力。
我们可以枚举字符串中的每一个或两个字符作为回文串的中心,然后向外扩展直至两端字符不同,这样就可以在 的时间复杂度内求出答案了。
而且,有些出题人比较懒,造的数据比较水,暴力可能跑得飞快。
但是,如果出题人存心要卡你呢?例如下面这个数据:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa......aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa(50000个a)
这时候,就要用 算法了。
一个简单的预处理
在暴力的过程中,我们可以发现一个很烦人的问题:回文串长度的奇偶性。这样就需要操作两次,非常麻烦。
怎么办呢?
我们就需要进行一波预处理。
以一个字符串"HLAKNOI"为例,为了解决回文串长度的奇偶性问题,我们可以在该字符串的每两个字符中间插入一个不可能在字符串中出现的字符(随便什么字符都可以,推荐使用’%’,毕竟’%’(膜)可以带来好运),并在首尾各加入一个不同的且同样不可能在字符串中出现的字符(这样一来就不需要判边界了)。
于是,原来的字符串就变成了这样:"!%H%L%A%K%N%O%I%?"。
这样,再去求回文就方便了许多。
一个简单的小性质
我们可以用 数组来记录从预处理后每个字符出发的回文半径,则对于一个字符串"abcbad",它的 数组如下:
! | % | a | % | b | % | c | % | b | % | a | % | d | % | ? | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 1 | 2 | 1 | 2 | 1 | 6 | 1 | 2 | 1 | 2 | 1 | 2 | 1 | 1 |
仔细观察可以发现,每个字母 值减 ,就是以这个字母为中心所能得到的最长回文串长度!
根据这个性质,不难想到,只要求出 数组,我们就能快速地求出整个字符串中最长的回文串长度了。
算法的核心思想
使用了一个简单的预处理,了解了一个简单的小性质,我们就可以开始理解 算法的核心思想 了。
首先,我们要考虑一个问题:暴力为什么跑得慢?
答案似乎很简单,因为暴力在求解的过程中,对很多位置会重复访问多次。
那么,能不能只访问一次呢?
算法正是建立于这个想法之上的。
算法的大致思路
第一步,我们用一个变量 来记录已经求解过的位置所产生的回文串能到达的最大的右边界,并记录下该回文串的中心位置 。
现在,假设我们要对一个新位置 求解,由于我们是从左到右操作的,因此可以保证 ,但是, 与 的大小关系就难以确定了,因此我们要对其进行分类讨论:
-
- 对于这种情况,说明以 为中心的回文串还没有被求解过,因此我们就对 暴力求解。
- 虽然这种情况的操作时间复杂度较高,但是要注意,这种情况出现次数越多,平均操作时间就越少,大致是成反比的,因此时间复杂度还是可以保证的。
-
- 对于这种情况,我们可以找到 关于 的对称点 ,然后对以 为中心的最长回文串的长度再次进行分类讨论:
- 以
为中心的最长回文串的长度较小,这个回文串在以
为中心的最长回文串的左半部分范围之内
- 由于回文串的性质,我们可以确定
- 因此,我们可以将 赋值为 ,然后继续向两边扩展
- 以
为中心的最长回文串的长度较小,这个回文串在以
为中心的最长回文串的左半部分范围之外
- 这时,我们只能保证在以 为中心的最长回文串的右半部分范围之内的以 为中心的串是回文串
- 因此,我们可以将 赋值为 。
- 其实上面两种情况我们可以直接概括为将 赋值为 。
算法的大致思路就是这样,如果还是有地方不懂,可以自己再多想想理解一下。
代码实现
class Class_Manacher//Manacher算法
{
private:
int len,p[(N<<1)+5];
char ns[(N<<1)+5];
inline void Init(string st)//一个简单的初始化
{
register int i,l=st.length();
for(ns[i=0]='!',ns[len=1]='%';i<l;++i) ns[++len]=st[i],ns[++len]='%';
ns[++len]='?';
}
public:
inline int GetAns(string st)//求解
{
register int i,ans=0,id,Max=0;
for(Init(st),i=1;i<len;++i)
{
p[i]=i<=Max?min(p[(id<<1)-i],p[id]+id-i):1;//分情况讨论,求出p[i]的初值
while(!(ns[i-p[i]]^ns[i+p[i]])) ++p[i];//向外扩展
if(i+p[i]>Max) Max=i+p[id=i];//更新所能到达的最大右边界
}
for(i=1;i<len;++i) ans=max(ans,p[i]);//求出最大的p[i]
return ans-1;//最大的p[i]减1才是答案
}
}Manacher;