回文串介绍

定义

  • 若一个字符串和它的逆串相同,例如$aba$,$acbca$,$acca$,那么满足这个性质的字符串被称为回文串。

性质

  • 对称性: $S$ 总是满足 $S_i=S_{n-i+1}(i \le n)$。
    image.png
  • 奇偶性:回文串可以分为奇数长度和偶数长度两种类型,长度为偶数的回文串的对称中心是一个空字符,而奇数长度的字符串对称中心是第 $S_{\frac{n+1}{2}}$ 个字符。
    image.png
  • 一个字符串本质不同的回文串个数不超过 $O(n)$。
  • 二分性:回文串的长度是支持二分的,因为回文串在两边删除相等的字符,仍然是一个回文串。

例题1

  • 给定一个长度不超过1000的字符串 $S$,输出最长的回文串长度

中心枚举

由于回文串都是中心对称的,因此我们可以枚举所有可能的中心位置,然后不断向两边添加相同字符,最后记录得到的最长的串。

image.png

我们发现,奇数长度的字符串中心非常好枚举,但是偶数长度的就有点点麻烦,因为偶数长度的中心不是一个字符而是在两个字符之间,因此我们考虑将这个字符串做一个预处理,在字符串的开头,结尾,每两个字符之间都添加一个原本字符串中不包含的特殊符号,使得字符串变成如下的形式

image.png

经过预处理后,原串中长度为偶数的串对称中心就变为了"#"字符,稍微分析可以发现,预处理后的字符串不包含长度为偶数的回文串。由此,我们可以枚举回文串的中心是哪一个字符,然后不断的向两边扩展,并且记录最长的长度 $len$,但是注意到,因为新串当中添加了字符"#",因此原串的最长回文串长度为$len$ 减去其中"#"字符的个数。总体时间复杂度 $O(n^2)$

例题2

  • 给定一个长度不超过$10^6$的字符串 $S$,输出最长的回文串长度

Manacher算法

预处理

  • 原来的中心枚举算法效率并不高,1975年,一个叫Manacher的人发明了一个算法---Manacher算法,利用了回文串的对称性质,将上述中心枚举算法的时间复杂度从 $O(n^2)$ 优化为了 $O(n)$
  • 利用和中心对称方法相同的预处理方法,将字符串里所有的奇数长度回文串变成了偶数长度。

回文串半径

  • 定义数组 $p$,$p_i$ 表示以字符串的第 $i$ 位为中心的最长回文串半径

image.png

如果求得 $p$,那么很快可以得到答案,现在考虑如何快速求 $p$

对称性

  • 在原来的中心枚举算法当中,我们是在当前位置 $i$,利用双指针向两边扩展,现在定义了数组 $p$,那么左指针就是 $i - p_i$,右指针就是 $i+p_i$。

    image.png

    但是在原来的算法当中,左右指针都是从 $i-1$ 和 $i+1$ 开始的,即都是从 $p_i=1$开始枚举的,现在有没有办法不从1开始枚举呢。

  • 之前的中心枚举算法,我们从$1$ 到 $n$ 的枚举中心。向两侧扩张然后记录最长回文串,现在我们在记录最长回文串的过程中,将枚举过的最长回文串的中心记为 $id$ 和右边界记录为 $mx$。

image.png

  • 由于回文串具有对称性,因此 $p$ 的初值也应该具有某种对称性,我们分类讨论如下,假设当前枚举中心为 $id$,需要求的位置 $p_i$

    • 若 $mx > i$
      说明需要求的位置在枚举过的最长回文串内部,那么根据回文串的对称性,找到关于 $id$ 对称的位置$2*id-1$ ,那么 $p_i$ 的值就可以利用 $p_{2*id-1}$得到。
      同时,就算是对称,只保证以 $i$ 为中心,右边界最大为 $mx$ 的范围内是回文串,因此结果有边界还需要在 $mx$ 范围内,因此得到

      $$p_i = min(p_{2*id-1}, mx - i)$$

      这个时候再暴力的向两边扩张得到最终的 $p_i$ 值

    • 若 $mx <= i$
      说明当前的位置并没有被枚举过的最长回文串包围,因此没法利用对称性,这个时候

      $$p_i=1$$

      再暴力的向两边扩张得到最终的 $p_i$ 值

  • 在所有的$p_i$ 值得以后,求解答案的方法和中心对称一样

时间复杂度 $O(n)$

大佬写的非常详细

总结

Manacher我个人理解是对中心枚举算法的一种优化,利用回文串的对称性质,快速得到 $p_i$ 的较好的初始值,而不是每一次都从1开始枚举。

代码

int manacher(string s){
    string temp = "$#";
    for(int i=0; i<s.length(); i++){
        temp += s[i];
        temp += '#';
    }
    vector<int> p(temp.length(),0);
    int mx = 0,id = 0,len = 0,cter = 0;
    for(int i=1; i<temp.length(); i++){
        p[i] = mx > i ? min(p[id*2-i],mx-i) : 1;
        while(temp[i+p[i]] == temp[i-p[i]]) ++p[i];
        if (mx < i + p[i]){
            mx = i + p[i];
            id = i;
        }
        if (len < p[i]){    
            len = p[i];
            cter = i;
        }
    }
    return len-1;
    //return s.substr((center-len)/2,len-1);
}

回文树

对于上述的问题当然不止一种解决方法,我们将介绍另一个处理字符串的强有力的工具,回文树。字符串 $caacbca$ 的回文树形态如下,接下来我们会一步步拆解出如何构建回文树。

image.png

简介

  • 回文树包含了给定字符串中所有的本质不同回文字串
  • 回文树节点个数不超过 $O(n)$
  • 回文树有两个根节点,两棵树,一棵树描述奇数长度回文串一个描述偶数长度回文串

定义

  • 节点

    回文树上的每一个节点都代表一个回文串
    image.png

  • 边均为有向边$(u,v,ch)$ 表示在节点 $u$ 所代表的回文串两边添加字符 $a$ ,得到 $v$ 所代表的回文串
    image.png

  • $fail$

    $fail_i$ 表示第 $i$ 个节点所代表得回文串的最长后缀回文串
    image.png

回文树可以拆分成上述三个部分,那么现在开始一步步构建回文树

构建

回文树的构建是在线一个字符一个字符插入的。假设已经对$S$的某个前缀 $P$ 建好了回文树,前缀 $P$ 的最长后缀回文串记录为 $Q$,$Q$的前一个字符为 $y$,现在需要新插入字符 $x$。
image.png

我们考虑新加入 $x$ 会新增加多少回文串。现在的回文树已经包含了所有前缀的回文串,在添加 $x$ 字符后新的回文串必定以 $x$ 字符为结尾。

  • 若 $x=y$
    那么 $yQx=xQx$,并且 $Q$ 也是一个回文串,那么 $yQx$ 就是新产 生的回文串
    image.png
  • 若 $x \neq y$
    这个时候,$yQx$ 不是回文串了,虽然 $y+Q$ 没办法让 $x$ 构成回文串了,但是$Q$是后缀上最长的回文串,后缀上还有第二长第三长的回文串有可能可以让 $x$ 构成回文串。
    这个时候发现, $Q$的$fail$指向的是$Q$的最长后缀回文,也就是 $P$ 的第二长后缀回文,那么令 $Q' = fail_Q,y'=fail_Q$的前一个字符

    这时候,又重新回到了上述判断,$y'$ 是否等于 $x$,重复以上步骤,直到找到某个 $y'=x$为止。
    image.png

通过上述的步骤我们找到了新增加的回文串 $xQx$,这个时候还差最后一步,就是找到新增加回文串对应的$fail$,因为 $xQx$ 的最长后缀回文也是以 $x$ 为结尾,因此不断的从 $Q$继续跳$fail$,直到找到一个位置 $xQ'x$,这个节点就是$xQx$节点的$fail$指向的节点。
image.png

按照上述步骤,将每一个字符都插入,就得到了$S$的回文树

应用

例题2

我们在建立回文树的过程中会记录每个节点代表的回文串长度,那么我们遍历所有节点取最大值即可,回文树总节点个数 $O(n)$,时间复杂度 $O(n)$

例题3

现在给定了一个字符串长度$10^6$,包含0-9数字,求所有回文字串对应的数字对 1e9 + 7取模的和。

这种题目Manacher也可以做,但是非常的麻烦,回文树就非常简单处理,在新添加节点的时候,记录新节点的价值为 $v_i$,父亲节点价值为 $v_{fa}$,那么就有

$$v_i = x*10^{len(v_{fa})+1}+v_{fa}*10 + x$$

求和即可,这些在Manacher内部是非常难以实现的。

代码

struct Palindrome_Aho{
    int len[maxn],nex[maxn][26];
    int fail[maxn],s[maxn];
    int n,totNode,last,cur;
    LL ans,cnt[maxn];
    int newnode(int p){
        for(int i=0; i<26; i++) nex[totNode][i] = 0;
        cnt[totNode] = 0; len[totNode] = p;
        return totNode++;
    }
    void init(){
        totNode = last = n = 0;
        newnode(0); newnode(-1);
        s[0] = -1;
        fail[0] = 1;
    }
    int Fail(int x){
        while(s[n-len[x]-1] != s[n]) x = fail[x];
        return x;
    }
    void insert(int ch){
        s[++n] = ch;
        cur = Fail(last);
        if (!nex[cur][ch]){
            int now = newnode(len[cur]+2);
            fail[now] = nex[Fail(fail[cur])][ch];
            nex[cur][ch] = now;
        }
        last = nex[cur][ch];
        cnt[last]++;
    }
    void count(){
        for(int i=totNode-1; i>=0; i--){

        }
    }
}pam;


TongChu
1 声望0 粉丝

练习编程两年半的个人练习生