关于回文树的理解

前言

这段时间搞字符串上了瘾?
看起来是的
那就继续搞吧

Part1一些名词

回文串

不想解释什么意思

回文子串

一个串的子串,它是回文串,那么它就是回文子串

最长回文后缀

对于一个长度小于自己的后缀,如果它是回文串,并且不存在比它更长的回文后缀,那么它就是最长回文后缀

最长回文前缀

基本和上面一样

Part2 回文树的形态

长成啥样啊?

我们很容易知道,回文串有两种,一种长度是奇数,一种长度是偶数
而在回文树上走,我们肯定不是一次只在后面添加一个字符
显然是在前后各添加一个字符
所以我们不难得出一点,如果串可以变成另外一个回文串
那么它的长度一定加上了一个偶数
所以在回文树上,为了区分这两种不同的回文串
所以回文树相当于一个森林
有两棵树,一棵的代表长度为奇数的回文串,另一棵代表长度为偶数的回文串

就像后缀自动机,Trie树,AC自动机这些东西一样
每一个节点代表的都是一个(些)串
回文树的每个节点也是代表着一个串
对于每个点的转移,比如说
对于某个点代表的回文串”aba”
假如它有一个’c’的转移
那么,”aba”就会指向一个代表着”cabac”的串

同样的,类似于AC自动机有 fail ,后缀自动机有 parent
当失配的时候回文树也有 fail 向上跳
那么,我们来考虑一些这个东西是什么?

假设当前加入的位置是 r
如果之前已经匹配出了一个回文Sl..r1
那么,如果有 Sl1=Sr 就没有失配
如果失配了,因为 r 位置是不能变动的
所以挪动的只有l位置
Sl..r 显然也要是一个回文串
所以 l 挪动到的位置就是Sl..r1的最长回文后缀的开始位置

一些小小的结论

综上所述,我们知道了两点:
1.对于回文树上的两个节点,如果存在字符c的连边,那么,就会从串x,变成cxc
2.对于回文树上的失配(fail)指针,指向这个点所代表的字符串的最长回文后缀所在的节点

一些小小的证明

接下来,我们还可以知道几点
1.对于任何一个串S,它的本质不同的回文串的个数不会超过|S|个
2.如果在串S后面加入一个字符,新增的本质不同的回文串的个数不会超过1个

怎么证明?

利用数学归纳法来证明

|S|=1 时,显然成立

如果我们知道 |S|=x1 时成立,现在插入 x 位置,字符为c
如果以 x 位置结尾出现了两个新的本质不同的回文串
假设较长的从l1开始,较短的从 l2 开始
因为 |Sl1..r|>|Sl2..r|
又根据回文串对称的性质
所以 Sl2..r Sl1..l1+rl2 必定出现过
所以不存在两个本质不同的回文串
所以最多新增一个本质不同的回文串
所以到 x 位置出现的本质不同的回文串的个数最多为x

同时,我们也证明了每次插入一个新的字符,最多增加一个本质不同的回文子串

Part3 回文树的构造

看完了上面,应该就知道了回文树上的东西代表着什么
我们的构造采用增量法,也就是类似于后缀自动机的 extend

假设前面已经构造出了 1..x1 的回文树,现在要加入第 x 个字符c
因为要接在 x1 的后面,我们又知道最多一个产生一个新的本质不同的回文串
也就是 S1..x1 中,最长的某个回文后缀 Sl..x1
同时能够满足 Sl1=Sx
因为只需要不停地寻找最长回文后缀

根据回文树上的 fail 指针的含义
我们很容易知道知道,
只需要从上一个位置添加完之后的最后一个位置
(也就是以 x1 为结束位置的最长回文子串)
所代表的节点开始,沿着 fail 一路上跳
检查是否满足 Sl1=Sx 就行了

假设这样找到的一个位置是 p
不难证明这个位置p一定存在(为啥?长度为1的回文串呀)
如果 p.son[c] 也就是连边 c 已经存在
那就什么都不用干,因为这个回文子串已经存在过
不需要重新建边
否则,重新建一个点表示这个回文子串,假设点是np
然后 p.son[c]=np

现在我们要找 np fail
因为要找的是最长回文后缀,不能是自己
所以令 k=p.fail
然后就像前面一样的,找到第一个满足 S[lk1]=S[n] 的点
k 沿着fail向上跳
然后 np.fail=k ,表示找到啦

这样,我们的回文树就利用增量法构建出来啦

当然,两棵树的根节点的长度分别是 1 0
然后为0的根节点的 fail 连向 1 的根节点
1 个根节点的 fail 也连向自己
为啥?自己想
初始情况下的 last=0,tot=1 (代表什么可以参考程序)

这是一棵回文树

struct Palindromic_Tree
{
    struct Node
    {
        int son[26];
        int ff,len;
    }t[MAX];
    int last,tot;
    void init()
    {
        t[++tot].len=-1;
        t[0].ff=t[1].ff=1;
    }
    void extend(int c,int n)
    {
        int p=last;
        while(s[n-t[p].len-1]!=s[n])p=t[p].ff;
        if(!t[p].son[c])
        {
            int v=++tot,k=t[p].ff;
            t[v].len=t[p].len+2;
            while(s[n-t[k].len-1]!=s[n])k=t[k].ff;
            t[v].ff=t[k].son[c];
            t[p].son[c]=v;
        }
        last=t[p].son[c];
        size[t[p].son[c]]++;
    }
};

Part4 后记

这篇博客十分简短
因此肯定有很多很多不严谨的地方
更加详细的请参考
国家集训队 2017 年的论文

当然了,这些东西也只是我自己的理解
而回文树也有很多很神奇的用法,
等我做了一些题之后我会再回来写的。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值