后缀自动机

后缀自动机


题目描述

洛谷P3804


核心思路

考虑如何求出 “子串的出现数量”

结论:子串substr出现的数量=|endpos(substr)|

也就是说,子串substr出现的数量,其实就是endpos(substr)这个集合中的元素的个数

如下图感性理解一下:

image-20210815164444420

如上图所示,设原串为 a b c a abca abca,我们想要求出子串a的出现数量,由原串一眼就看出子串a出现了两次。但是我们观察endpos发现, e n d p o s ( " a " ) = [ 1 , 4 ] endpos("a")=[1,4] endpos("a")=[1,4],这个集合中的元素个数为2,这刚好就说明了子串a出现了两次。

接下来就要考虑怎么才能求出某个endpos集合的大小呢?

这里要注意,其实endpos集合只与后缀自动机中的后缀链接link有关,因此我们建图时,就只需要把含有link的边建立出来就好了。

image-20210815165154096

如上图所示,我们发现一个性质:叶子节点endpos的并集就构成了它们父节点的endpos中的元素。但是要注意有可能父节点是有属于自己特有的元素的。因此我们要计算endpos[u]的大小时,就先计算自己特有的元素的大小endpos[u],然后再计算它的子节点的元素大小endpos[v],那么相加,就会得到状态节点 u u u的endpos集合的大小 ∣ e n d p o s ( u ) ∣ |endpos(u)| endpos(u)

虽然再后缀自动机中,我们是从子节点向父节点引了后缀链接边,但是在这题中,我们建图时,我们是从父节点向子节点建立一条有向边。为什么呢?因为这样当我们进行dfs时,从根节点开始,一直递归到叶子节点,然后统计出了叶子节点 v v v的endpos集合大小 e n d p o s ( v ) endpos(v) endpos(v),那么在回溯时就会累加到其父节点 u u u,那么就可以算出父节点 u u u的endpos集合大小 e n d p o s ( u ) endpos(u) endpos(u)


代码

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
//N是后缀自动机中的状态节点个数 要开2倍  M是建立出只含link后缀链接时图中边的总数
const int N = 2e6+10,M = N;
//tot记录的是后缀自动机中的状态节点 初始化为根节点1
//last记录的是上一个状态节点
int tot = 1, last = 1;
struct Node
{
    int len;    //记录这个状态节点所形成的串中最大的那个串的长度
    int link;   //后缀链接
    int ch[26]; //类似于trie树中的孩子
}node[N];
char str[N];
//endpos集合   ans是答案
LL endpos[N], ans;  
int h[N], e[M], ne[M], idx;

//后缀自动机模板
void extend(int c)
{
    //先用p来记录上一个状态节点
    //然后由于来了一个字符c,需要进行状态转移了
    // 因此给转移后得到的节点np分配一个编号tot
    int p = last, np = last = ++ tot;
    //以tot这个位置结尾形成串它自己就是一个前缀
    endpos[tot] = 1;
    //由于np是从p通过新添一个字符c转移过去的  因此长度+1
    node[np].len = node[p].len + 1;
    //沿着p的后缀链接遍历节点  如果遍历到的节点p它没有孩子c
    //则要创建出来  让np连一条后缀链接边到node[p].ch[c]就相当于
    //该节点p通过字符c转移到了np
    for (; p && !node[p].ch[c];p = node[p].link)
        node[p].ch[c] = np;
    //沿着后缀链接一直来到了根节点  仍然没有发现
    //那么np的后缀链接就是根节点
    if (!p)
        node[np].link = 1;
    else
    {
        int q = node[p].ch[c];  //找到状态节点p的c孩子节点 q
        //np沿着后缀链接找到了q 发现q中有np想要的串 能够进行后缀邻接
        //那么此时nq就可以引出一条后缀链接边到q
        if (node[q].len == node[p].len + 1)
            node[np].link = q;
        else
        {
            //将q一分为二 变成q 和 nq
            int nq = ++ tot;
            //nq是q克隆出来了  但是nq包含了从q中抽离出来的能够让np进行后缀链接的串
            node[nq] = node[q], node[nq].len = node[p].len + 1;
            //分裂前的q引一条后缀链接边到nq
            //从新开的状态节点np引一条后缀链接边到nq
            node[q].link = node[np].link = nq;
            //沿着p的后缀链接一直往回走 将所遍历到的节点 都 引一条后缀链接边到nq
            for (; p && node[p].ch[c] == q; p = node[p].link)
                node[p].ch[c] = nq;
        }
    }
}

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

void dfs(int u)
{
    for (int i = h[u]; ~i; i = ne[i])
    {
        int v=e[i];
        dfs(v);
        //当前状态节点u的endpos就等于它自身特有的endpos[u]加上它的孩子节点的endpos[v]
        //比如u={1,2,3,4,5}  v1={1,2},v2={3,4} 那么它特有的就是{5}
        //所以u的endpos集合中元素的个数 就是  它自己特有的{5} 加上 |v1|+|v2|
        //一共就是5个 即endpos[u]的元素个数为5
        endpos[u] += endpos[v];
    }
    //出现次数不为 1
    if (endpos[u] > 1) 
        ans = max(ans, endpos[u] * node[u].len);
}

int main()
{
    scanf("%s", str+1);
    for (int i = 1; str[i]; i ++ )
        extend(str[i] - 'a');
    memset(h, -1, sizeof h);
    //虽然后缀自动机中后缀链接是从子节点引向父节点
    //但是我们这里是建边时是从父节点引向子节点  因为这样的话
    //我们在dfs时是从父节点递归到子节点  然后统计子节点的endpos值
    //回溯时累加到其父节点的endpos上     这样就刚好满足 "父节点是它子节点的并集" 的性质
    for (int i = 2; i <= tot; i ++ )
        add(node[i].link, i);
    dfs(1); //从后缀状态机的根节点开始深搜
    printf("%lld\n", ans);

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值