HDU 4622 后缀自动机

题意

一个字符串,问一段区间内的不重复子串有多少个。

关于后缀自动机

这道题基本上算是后缀自动机的模板题了。但是后缀自动机看了好久,刘汝佳在那本紫书中提到了三个数据结构的难点,现在看来后缀自动机算是第二难的。(毕竟可持久化平衡树有rope)
后缀自动机的话,网上有很多教程,但是即便教程很多,后缀自动机也依然很难。这里强烈推荐fanhq后缀自动机教程
这篇教程将后缀自动机的构建转化成了后缀树的构建,这样的话就容易理解很多了(毕竟自动机是个高端货,而树就平易近人很多)
我们可以从多个角度来理解后缀自动机,首先的话,后缀自动机的是一个自动机,这个自动机,从ROOT点开始,走到所有点,所组成的字符串集合恰好是所有子串的集合。
其次的话,我们还可以将后缀自动机看成一棵后缀树,当然,这棵后缀树不同人也可以看出来不同的东西,主流的观点认为这棵后缀树是逆序串的后缀树,不过我们也可以认为这是一棵叶子节点为前缀集合的树。
具体如下图所示(根据字符串abcbc构建的后缀自动机):
后缀自动机
其中实线代表的是后缀树的边,虚线代表的是后缀自动机的边。很明显便能发现是符合上面的论述的。

题解

终于到了题解部分,关于这道题,我们还是要从那张图上找规律。我们可以发现后缀自动机(后缀树)有一个这样的规律,图片中的数字标记的是到这个点所组成的最长的字符串长度,例如c这个点可以组成bc和c两个字符串,我们便取最长的2作为这个点的值。然后我们便可以发现len[q]-len[par[q]]便是一个点新增的字符子串个数
比如说(ab)c这个点,由于这个点是从c这个点转移而来的,那么我们就要讨论后缀为C的字符子串。我们可以发现能形成3个子串,abc,bc,c,由于bc和c是属于c这个点的,所以实际上增加的字符子串只有一个。然后我们可以再去验证一个点,比如说我们可以验证(abcb)c这个点,我们可以得到五个子串,abcbc,bcbc,cbc,bc,c,我们可以发现bc和c是属于c这个点的,然后剩余的三个字串是这个点所独有的子串,因此新增的子串数量就是5-2==3。
很有趣的性质,我们可以简单分析一下为什么会出现这种有趣的性质。一切还要从构建这个后缀自动机说起。如果我们目前拥有了一个点,比如说bc,这时候要新增一个abc的点,如何保证子串bc和c不会重复计算呢?如果我们仔细读一下后缀自动机的代码就可以意识到,如果len(abc)==3且len(bc)==2,那么abc这个串一定会接到bc这个串的对应的点后面。这样的话,根据我们上面讨论的那个公式,只有abc这个串会被计算,所以不会产生重复的子串。
当然,有可能不会这么巧。比如说abcbc这个点,我们可以发现abcbc这个点要构建在abc这个点的后面(这里讨论的是后缀自动机构建时的过程,不是最终的结果)。如果直接添加在后面,那么就会存在子串数量统计少了的问题,因为abcbc并不是以abc为后缀的。这时候我们就需要提取后缀,我们发现公共后缀是bc,这样的话,我们可以将bc提取出来作为一个父节点,然后我们就成功解决了这个问题。
我们可以发现,这个规律和后缀自动机的构建过程是相互印证的。通过仔细分析这个规律,可以更好的理解为什么后缀自动机需要这样构造。

代码

#include <iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>
#include<cmath>
#include<queue>
#include<string>
#include<set>
#include<map>
#include<bitset>
#include<stack>
#include<string>
#define UP(i,l,h) for(int i=l;i<h;i++)
#define DOWN(i,h,l) for(int i=h-1;i>=l;i--)
#define W(a) while(a)
#define MEM(a,b) memset(a,b,sizeof(a))
#define LL long long
#define INF 0x3f3f3f3f
#define MAXN 4010
#define MOD 1000000007
#define EPS 1e-3
using namespace std;
char s[MAXN];
int sum[MAXN][MAXN];

struct Suffix_Auto {
    int allc,last,par[MAXN<<1],len[MAXN<<1],trans[MAXN<<1][26];
    int newNode() {
        int now=++allc;
        MEM(trans[now],0);
        return now;
    }
    void init() {
        allc=0;
        last=newNode();
        par[last]=len[last]=0;
    }
    int extend(int c) {
        int p=last,np=newNode();
        len[np]=len[last]+1;
        for(; p&&!trans[p][c]; p=par[p]) trans[p][c]=np;
        if(!p) par[np]=1;
        else {
            int q=trans[p][c];
            if(len[q]==len[p]+1) par[np]=q;
            else {
                int nq=++allc;
                par[nq]=par[q];
                len[nq]=len[p]+1;
                memcpy(trans[nq],trans[q],sizeof(trans[q]));
                par[np]=par[q]=nq;
                for(trans[p][c]=nq,p=par[p]; p&&trans[p][c]==q; p=par[p]) trans[p][c]=nq;
            }
        }
        last=np;
        return len[np]-len[par[np]];
    }
};
Suffix_Auto sa;

int main() {
    int t;
    scanf("%d",&t);
    W(t--) {
        MEM(sum,0);
        scanf("%s",s);
        int q;
        int len=strlen(s);
        UP(i,0,len) {
            sa.init();
            UP(j,i,len) {
                sum[i+1][j+1]=sum[i+1][j]+sa.extend(s[j]-'a');
            }
        }
        scanf("%d",&q);
        W(q--) {
            int a,b;
            scanf("%d%d",&a,&b);
            printf("%d\n",sum[a][b]);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值