后缀全家桶

· · 算法·理论

前言

后缀全家桶应用还挺多的。

正文

后缀数组

概念

后缀 i 表示 s[i...n]

$rk_i$ 表示后缀 $i$ 的排名。 它们之间有个性质:$sa_{rk_i}=rk_{sa_i}=i$. ### 求法 暴力求为 $O(n^2\log n)$ 显然不优。 我们可以用倍增的思想,先求出一个字符的排名,再求出之后两个字符的排名,以此类推。复杂度降到 $O(n\log^2 n)$ 。用基数排序优化可得 $O(n\log n)$ 。 [【模板】后缀排序](https://www.luogu.com.cn/problem/P3809) 输出 $sa$ 数组即可。 代码: ```cpp void SA(){ f(i,1,n) ++t[rk[i]=a[i]]; f(i,1,m) t[i]+=t[i-1]; F(i,n,1) sa[t[a[i]]--]=i; for(int k=1,T=0;k<=n;k<<=1,m=T,T=0){ f(i,n-k+1,n) p[++T]=i; f(i,1,n) if(sa[i]>k) p[++T]=sa[i]-k; f(i,0,m) t[i]=0; f(i,1,n) ++t[rk[p[i]]]; f(i,1,m) t[i]+=t[i-1]; F(i,n,1) sa[t[rk[p[i]]]--]=p[i]; swap(rk,p),rk[sa[1]]=T=1; f(i,2,n) rk[sa[i]]=(p[sa[i]]==p[sa[i-1]]&&p[sa[i]+k]==p[sa[i-1]+k])?T:++T; if(T==n) break; } int k=0; f(i,1,n){ if(rk[i]==1){h[1]=k=0;continue;} if(k) --k; for(int j=sa[rk[i]-1];i+k<=n&&j+k<=n&&a[i+k]==a[j+k];++k); h[rk[i]]=k; } } ``` ### 应用 #### LCP 记 $lcp(i,j)$ 表示 $s[i,n],s[j,n]$ 的最长公共前缀,$height_1=0,height_{rk_i}=lcp(i,i-1)$ 则 $\displaystyle lcp(i,j)=\min_{k=rk_i+1}^{rk_j}height_k

性质:height_{rk_i}\ge height_{rk_{i-1}}-1

本质不同的子串数目

\dfrac{n(n+1)}{2}-\displaystyle \sum_{i=2}^{n}height_i

后缀自动机

概念介绍

后缀自动机就是将一个能表示所有子串的 DAG 和一棵表示不同的 Endpos 的树(叫 Parent 树)共用点集的一个东西。

记录一下东西:

$len_i:$ $i$ 表示的字符串的最大长度。 $fa_i:$ $i$ 在 $Parent$ 树上的父亲。 $trie_{i,c}:$ $i$ 指向的节点。 有三个性质: 1. $DAG$ 上的一条路径表示一个子串。 2. 一个节点对应的所有串的 $Endpos$ 相同。 3. $Parent$ 树上一个点的父亲表示该点 $Endpos$ 不同的最长后缀。 #### 小应用 1. 一个点代表的串可以 $DAG$ 上 $dp$ 得到,也可以是 $len_i$ 和 $len_{fa_i}$ 之间的子串,具体看哪个写法方便维护信息 2. 在Parent 树上倍增可在 $O(\log |S|)$ 的时间复杂度内找到包含子串 $[l,r]$ 的节点 3. 求公共子串只要在 $SAM$ 走一遍就行,能走长度加 $1$ ,否则跳 $fa$ ,并且将长度赋为 $len_{fa}

构造方法

增量法。

已知 |S|SAM ,构造 |S|+cSAM

先新建一个节点 np ,设当前结尾为 p,q=trie_{p,c}

如果 |S| 中没有出现 c ,那么就从 |S| 代表的节点一直跳 fa ,每经过一个节点就连一条 c 的边,之后 fa_{np}=0

否则如果 len_p+1=len_q ,令 fa_{np}=q 即可。

否则 分裂 q ,设分裂出来的节点为 nq ,先将 q 的所有信息继承到 nq ,然后令 fa_{np}=nq,fa_q=nq,len_{nq}=len_p+1 , 最后将 pParent 树上的所有祖先中原本指向 q 的点连向 nq

原理:没有 c ,连 c 显然。

如果 $len_p+1=len_{q}$ 那么 $p$ 中最长的串 $+c$ 就是 $q$ 中最长的串,$fa_{np}=q$ 。 否则 $q$ 中还有比 $p$ 中最长的串 $+c$ 更长的串,将其分裂出去记为新 $q$ ,剩下的为 $nq$ ,则有 $len_{nq}=len_p+1$ 所以 $fa_{np}=nq$ ,而 $fa_{nq}$ 就为原 $fa_q$ ,新 $fa_q=nq$ ,最后将 $p$ 在 $Parent$ 树上的所有祖先中原本指向 $q$ 的点连向 $nq$ 即可。 具体看[这](https://www.cnblogs.com/zaza-zt/p/15419181.html) 代码: ```cpp void extand(int i,int c){ int p=now,q; for(len[now=++cnt]=i;p&&!trie[p][c];p=fa[p]) trie[p][c]=now; if(!p){fa[now]=1;return ;} if(len[q=trie[p][c]]==len[p]+1){fa[now]=q;return ;} len[++cnt]=len[p]+1; f(i,0,25) trie[cnt][i]=trie[q][i]; fa[cnt]=fa[q],fa[q]=fa[now]=cnt; for(int i=p;trie[i][c]==q;i=fa[i]) trie[i][c]=cnt; } ``` ## 伪广义后缀自动机 就是把一堆串中间插个奇怪字符拼一起跑 $SAM

广义后缀自动机

就是在 trie 树上跑 SAM ,支持处理多个串。

在线做法是将 lst=1 ,然后加两个特判。

代码:

int extand(int c){
    if(trie[lst][c]&&len[trie[lst][c]]==len[lst]+1)
        return trie[lst][c];
    int p=lst,q,now;bool ok=0;
    for(len[now=++cnt]=len[p]+1;p&&!trie[p][c];p=fa[p])
        trie[p][c]=now;
    if(!p){fa[now]=1;return now;}
    if(len[q=trie[p][c]]==len[p]+1){fa[now]=q;return now;}
    if(p==lst) ok=1,now=0,--cnt;
    len[++cnt]=len[p]+1;
    f(i,0,25)
        trie[cnt][i]=trie[q][i];
    fa[cnt]=fa[q],fa[q]=fa[now]=cnt;
    for(int i=p;trie[i][c]==q;i=fa[i])
        trie[i][c]=cnt;
    return ok?cnt:now;
}

后缀平衡树

我们将后缀排名插入平衡树即可得到后缀平衡树,特别地,后缀平衡树的中序遍历就是后缀数组。

如果这么理解就和后缀数组没啥区别了。

主要看如何在线构造,后缀平衡树支持在前端插入或删除字符。每次插入时,根据已有的后缀信息比较,每个后缀赋个权值即可,当不平衡时,暴力重赋权值。

代码:

#include<bits/stdc++.h>
#define f(i,l,r) for(int i=l;i<=r;++i)
#define F(i,r,l) for(int i=r;i>=l;--i)
#define LL long long
#define ULL unsigned long long
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
#define read(n) {int _x=0,_ty=1;char _c=getchar();while(!isdigit(_c)){if(_c=='-')_ty=-1;_c=getchar();}while(isdigit(_c))_x=10*_x+_c-'0',_c=getchar();n=_x*_ty;}
char buf[1<<21],*p1=buf,*p2=buf;
using namespace std;
const int N=800005;
const LL INF=1e18;
int n,rt,mask,ans;
char a[N],t[N];
struct node{
    int son[2],siz,rnd;
    LL val;
}tree[N];
inline void decode(char *ch,int mask){
    int l=strlen(ch);
    for(int i=0;i<l;i++){
        mask=(mask*131+i)%l;
        char t=ch[i];
        ch[i]=ch[mask];
        ch[mask]=t;
    }
}
inline int cmp(int x,int y){
    if(a[x]>a[y]||a[x]==a[y]&&tree[x-1].val>tree[y-1].val) return 1;
    if(a[x]==a[y]&&tree[x-1].val==tree[y-1].val) return 0;
    return -1;
}
inline bool check(int x){for(int i=1,j=x;t[i];++i,--j){if(t[i]<a[j])return 1;if(t[i]>a[j])return 0;}}
inline void pushup(int x){tree[x].siz=tree[tree[x].son[0]].siz+tree[tree[x].son[1]].siz+1;}
inline void rebuild(int x,LL l,LL r){
    tree[x].val=l+r>>1;
    if(tree[x].son[0]) rebuild(tree[x].son[0],l,tree[x].val-1);
    if(tree[x].son[1]) rebuild(tree[x].son[1],tree[x].val+1,r);
}
inline void rotate(int &x,bool op,LL l,LL r){
    int s=tree[x].son[op^1];
    tree[x].son[op^1]=tree[s].son[op];
    tree[s].son[op]=x;
    pushup(x),pushup(s),x=s;
    rebuild(x,l,r);
}
inline void insert(int v,int &x,LL l,LL r){
    if(!x){tree[x=v]=(node){{0,0},1,rand(),l+r>>1};return ;}
    bool ok=cmp(x,v)<0;
    insert(v,tree[x].son[ok],ok?tree[x].val+1:l,ok?r:tree[x].val-1);
    pushup(x);
    if(tree[x].rnd>tree[tree[x].son[ok]].rnd) rotate(x,ok^1,l,r);
}
inline int merge(int x,int y){
    if(!x||!y) return x+y;
    if(tree[x].rnd>tree[y].rnd) return tree[y].son[0]=merge(x,tree[y].son[0]),pushup(y),y;
    else return tree[x].son[1]=merge(tree[x].son[1],y),pushup(x),x;
}
inline void erase(int v,int &x,LL l,LL r){
    if(!x) return ;
    int ok=cmp(x,v);
    if(!ok){x=merge(tree[x].son[0],tree[x].son[1]);if(x)rebuild(x,l,r);}
    else erase(v,tree[x].son[ok<0],ok<0?tree[x].val+1:l,ok<0?r:tree[x].val-1);
    if(x) pushup(x);
}
inline int rnk(int x){
    if(!x) return 1;
    if(check(x)) return rnk(tree[x].son[0]);
    else return tree[tree[x].son[0]].siz+1+rnk(tree[x].son[1]);
}
int main(){
    srand(time(0));
    read(n);
    int len=0;
    char c=getchar();
    while(c<'A'||c>'Z') c=getchar();
    while('A'<=c&&c<='Z') a[++len]=c,c=getchar();
    a[len+1]=0;
    f(i,1,len)
        insert(i,rt,1,INF);
    while(n--){
        int x;
        char op,c=getchar();
        while(c<'A'||c>'Z') c=getchar();
        while('A'<=c&&c<='Z') op=c,c=getchar();
        if(op=='D'){
            int l=0;
            char c=getchar();
            while(c<'A'||c>'Z') c=getchar();
            while('A'<=c&&c<='Z') t[++l]=c,c=getchar();
            t[l+1]=0;
            decode(t+1,mask);
            f(i,len+1,len+l)
                a[i]=t[i-len],insert(i,rt,1,INF);
            len+=l;
        }
        else if(op=='L'){
            read(x);
            F(i,len,len-x+1)
                erase(i,rt,1,INF);
            len-=x;
        }
        else{
            int l=0;
            char c=getchar();
            while(c<'A'||c>'Z') c=getchar();
            while('A'<=c&&c<='Z') t[++l]=c,c=getchar();
            t[l+1]=0;
            decode(t+1,mask);
            reverse(t+1,t+l+1);
            t[l+1]='Z'+1,t[l+2]=0;
            ans=rnk(rt);--t[l];
            ans-=rnk(rt);
            mask^=ans;
            printf("%d\n",ans);
        }
    }
    return 0;
}

后缀树

S 的所有后缀插入 trie 中就得到了后缀 Trie,后缀 Trie 可以表示 S 的所有子串。但暴力为 O(n^2) ,考虑压缩。令后缀 trie 中所有拥有多于一个儿子的节点和后缀节点为关键点。只保留关键点,将非关键点形成的链压缩成一条边形成的压缩 trie 树就为为后缀树。复杂度 O(n)

建法:反串跑SAM的Parent树即为后缀树。