后缀树的线性在线构建-Ukkonen算法

Ukkonen算法是一个非常直观的算法,其思想精妙之处在于不断加字符的过程中,用字符串上的一段区间来表示一条边,并且自动扩展,在需要的时候把边分裂。使用这个算法的好处在于它非常好写,代码很短,并且它是在线的,时间复杂度为\(O(n)\) ,是后缀树构建算法的佳选。

算法

我们保存当前节点now的位置,以及剩下还没有实际上插入的后缀数量remain。设当前字符串中已插入的字符数量为\(n\)

最开始remain+1,n+1,代表当前字符串中多了一个字符,多了一个需要插入的后缀。很明显,当前我们要插入后缀的长度为remain,因为后缀是连续的。所以这个后缀的开头位置为n-remain+1 。如果当前要插入后缀的长度大于当前出边的长度,那么不断往后跳直到符合要求。

这时有三种情况:

  • 不存在需要的出边,那么我们直接加边即可。
  • 存在需要的出边,并且所需字符与边上的字符相同,即要插入的后缀被隐含在这条边中了,那么我们退出
  • 存在需要的边,但所需字符与边上字符不同。这时候我们就需要分裂这条边。
  • 如果插入了边,并且当前点为root,那么remain-1

这时候有一个很明显的问题,如果我们每一次都退回根节点重新查找,那么时间复杂度可以达到\(O(n^2)\)。但我们可以发现一个性质,当前插入的这个后缀的下一个后缀就是我们要插入的下一个后缀。比如说,我们当前插入了abc这个后缀,那么下一个插入后缀必定是bc。这样,我们每次可以把这一次add中的上一个点通过一种特殊的后缀连接连到这个点,那么我们就可以快速跳link来找下一个插入位置了。

如果我们处理的是子串,那么这样就够了,但是如果我们处理的是后缀,那么还需要在最后加入一个没有出现过的字符来把所有的隐含点拿出来。

时间复杂度

每次跳link的时候需要插入的长度其实都是在减的,而需要插入的长度一共最多为\(O(n)\),所以跳来跳去的部分复杂度为\(O(n)\)。而我们注意到每次插入的复杂度都是\(O(1)\)的,并且后缀树的节点个数最多为\(2n-1\),所以插入也是\(O(n)\)的,因此总的复杂度为\(O(n)\)

资料

在学这个算法的过程中找到了很多资料,选几个比较好的出来分享一下:

Visualization of Ukkonen's Algorithm: 一个非常棒的算法可视化的动画

Ukkonen算法模拟与教程: 良心作者,大家给他点赞!!(后面那个Github上的代码是错的不要学)

代码

这是bzoj3238的代码。不用管graph那一块啦,后缀树就是ST。

#include
#include
#include
#include
#include
using namespace std;
typedef long long giant;
const int maxc=28;
const int maxn=1e6+10;
giant ans;
char s[maxn];
struct graph {
    struct edge {
        int v,w,nxt;
    } e[maxn];
    int h[maxn],tot,dep[maxn],size[maxn];
    graph ():tot(0) {}
    void add(int u,int v,int w) {
        e[++tot]=(edge){v,w,h[u]};
        h[u]=tot;
    }
    void dfs(int x,int fa) {
        size[x]=0;
        bool flag=false;
        for (int i=h[x],v=e[i].v;i;i=e[i].nxt,v=e[i].v) {
            flag=true;
            dep[v]=dep[x]+e[i].w;
            dfs(v,x);
            ans-=(giant)dep[x]*size[x]*size[v]*2ll;
            size[x]+=size[v];
        }
        if (!flag) {
            --dep[x];
            size[x]=(dep[x]>dep[fa]);
        }
    }
} G;
struct ST {
    const static int inf=1e8;
    int t[maxn][maxc],len[maxn],start[maxn],link[maxn],s[maxn],tot,n,rem,now;
    ST ():tot(1),n(0),rem(0),now(1) {len[0]=inf;}
    int node(int sta,int l) {
        start[++tot]=sta,len[tot]=l,link[tot]=1;
        return tot;
    }
    void add(int x) {
        s[++n]=x,++rem;
        for (int last=1;rem;) {
            while (rem>len[t[now][s[n-rem+1]]]) rem-=len[now=t[now][s[n-rem+1]]];
            int ed=s[n-rem+1];
            int &v=t[now][ed];
            int c=s[start[v]+rem-1];
            if (!v) {
                v=node(n-rem+1,inf);
                link[last]=now;
                last=now;
            } else if (x==c) {
                link[last]=now;
                last=now;
                break;
            } else {
                int u=node(start[v],rem-1);
                t[u][x]=node(n,inf);
                t[u][c]=v,start[v]+=rem-1,len[v]-=rem-1;
                link[last]=v=u,last=v;
            }
            if (now==1) --rem; else now=link[now];
        }
    }
    void run() {
        for (int i=1;i<=tot;++i) for (int j=1;j>1;
    G.dfs(1,0);
    printf("%lld\n",ans);
    return 0;
}

转载于:https://www.cnblogs.com/owenyu/p/6875887.html

你可能感兴趣的:(后缀树的线性在线构建-Ukkonen算法)