manacher算法

manacher,是一种线性时间复杂度求解回文字符串的算法。而且很短。

首先我们观察回文串,发现有长度为奇数的(以某个字符为对称中心)和长度为偶数的(以两个字符间的空位为对称中心)。为了方便我们把它们转化成一样的,我们在字符之间插入一个占位符#,这样所有回文串就变成了奇数长度。

然后观察转移的方式。我们根据回文串的对称性质,可以用对称中心左边已经处理过的字符的回文半径\(f\)(即从对称中心到回文串一边的字符个数)转移对称中心右边与它对称的回文半径。

\[\ldots \overbrace{ s_l\ \ldots \underbrace{s_{j-f[j]+1}\ \ldots\ s_j\ \ldots\ s_{j+f[j]-1}\ }_{\text {palindrome}}\ \ldots\ \underbrace{s_{i-f[j]+1}\ \ldots\ s_{i}\ \ldots\ s_{i+f[j]-1}}_{\text {palindrome}}\ldots\ s_r }^{\text{palindrome}} \]

(我现在才知道oiwiki的这个看起来像图的东西是个\(\LaTeX\)

观察可以得到,左边的\(s_j\)和右边的\(s_i\)对称,所以可以将\(f[j]\)直接转移到\(f[i]\)。但是还有这样一种情况,就是我们左边的回文串超过了与当前回文串左端点的距离,那么就只能将右边的回文串转移到右端点的位置,而不能直接转移\(f[j]\)

最后,为了尽可能转移更多的字符,我们需要找到右端点在最右边的一个回文串。这个保存一个指针扫就可以。

上个代码。

int main(){
    scanf("%s",ch+1);len=strlen(ch+1)*2+1;//一定要+1保证头尾都是#
    int r=0,mid=0;s[0]='~';//s[0]插入不同字符保证数组不越界
    for(int i=1;i<=len;i++)s[i]=(i&1)?'#':ch[i>>1];//在字符中插入占位符
    for(int i=1;i<=len;i++){
        if(i<=r)f[i]=min(f[2*mid-i],r-i+1);//更新答案
        while(s[i+f[i]]==s[i-f[i]])f[i]++;//暴力扩展
        if(i+f[i]>r)r=i+f[i]-1,mid=i;//更新指针
        ans=max(ans,f[i]);
    }
    printf("%d",ans-1);/*回文串真实长度
    这个可以分类讨论一下 如果真实长度是奇数那么占位符一定比字符多1
    而(字符数+占位符数)=2*回文半径-1
    把+1和*2扔到左边 因为是奇数然后+1 /2答案就大了1 所以要-1
    然后考虑偶数的情况 占位符仍然比字符多1 可以按类似的方法计算*/
}

这个算法的时间复杂度为什么是\(O(n)\)的呢?观察我们外层循环的\(i\)指针是扫了一遍的,而我们指向回文串最右端的\(r\)指针随着扫描而单调递增,所以两个指针都只线性扫了一遍,所以是\(O(n)\)的。

posted @ 2022-09-03 19:48  gtm1514  阅读(24)  评论(1编辑  收藏  举报