UOJ Logo oscar的博客

博客

UOJ#35 后缀排序 SAM做法

2017-08-29 02:12:38 By oscar

大家好我是oscar..这可能是我第一次发博客诶..

由于我比较弱,所以可能讲了一些大家都会的东西...求不D...

好了不说太多,先步入正题...

今天我们的挑战是:不建后缀数组完成任务(网上的做法好像都是建出后缀数组再用后缀数组的性质算出height数组)

假装大家都会后缀自动机啦(我这种蒟蒻花了约一个星期才搞懂)

首先将 从last到root用suffix link连接的路径上的节点 标上结束标记(

inline void update()
{
    node *p=last;
    while(p!=root)
    {
        p->isend=1;
        p=p->link;
    }
}

由于后缀自动机上所有路径能表示出原串的所有子串,所有以 一个有标记节点 结束的路径能表示出原串的所有后缀

于是可以从根节点DFS一遍,每次有分岔时优先走字典序比较小的转移,途中遇到有结束标记的节点则输出(原串总长度-DFS到当前节点经过长度+1),这样就完成了第一部分的任务(?)

第二部分的任务是找出排序后相邻两个后缀的最长公共前缀...我们发现在第一部分的DFS中正好排序后位置相邻的后缀访问顺序也是连着的,只需要求相邻两个访问的后缀的“LCA”就可以啦

贴代码走人

鬼故事,进度条

int lcp[MAXN],minn,cnt;
void dfs(node *u,int len=0)
{
    if(u->isend)
    {
        printf("%d ",totlen-len+1);//totlen为字符串总长
        lcp[++cnt]=minn;            //更新LCP信息
        minn=len;
    }
    for(int x=0;x<sigma;x++)
    {
        if(u->next[x])
        {
            if(len<minn)minn=len;//用来记录LCP长度
            dfs(u->next[x],len+1);
        }
    }
}

呃...等等...为什么我TLE了?

貌似这样搜索会做好多重复工作诶...(应该是 $ O(n^2) $ 的)

我们来想办法优化一下

由于不需要输出路径上的所有字符,所以只需要考虑路径长度

那么只有一种走法的路径就可以被压缩成一条边

也就是说这样的路径↓

$ \Huge \bigcirc ---1--> \bigcirc ---1--> \bigcirc ---1--> \bigcirc $

可以压缩成这样↓

$ \Huge \bigcirc ---3--> \bigcirc $

而不改变拓扑结构DFS结果

这...就是...传说中的...路径压缩?

typedef pair<node*,int> pni;
pni find(node *u)
{
    if(!u->fast)return make_pair(u,0);
    pni _=find(u->fast);
    u->fast=_.first;
    u->fastlen+=_.second;
    return make_pair(u->fast,u->fastlen);
}

对比一下并查集的路径压缩(大雾)

typedef pair<int,int> pii;
pii find(int x)
{
    if(!pa[x])return make_pair(x,shift[x]);
    pii _=find(pa[x]);
    pa[x]=_.first;
    shift[x]^=_.second;
    return make_pair(pa[x],shift[x]);
}

好像没啥区别(大雾)

还需要预处理一下哪些边可以被压缩掉(动态求好像是错的,我不太明白为什么,可能是我写挂了),

把代码扔进一开始的update函数里

inline void update()
{
    node *p=last;
    while(p!=root)
    {
        p->isend=1;
        p=p->link;
    }
   //更新部分↓
    for(int i=1;i<=top;i++)
    {
        node *t=&pool[i],*q=0;
        if(t->isend)continue;
        int c=0;
        for(int ch=0;ch<sigma;ch++)
        {
            if(t->next[ch])
                ++c,q=t->next[ch];
        }
        if(c==1)
        {
            t->fast=q;
            t->fastlen=1;
        }
    }
   //更新部分↑
}

最后在搜索的时候判一下能不能走被压缩的路径就好啦

int lcp[MAXN],minn,cnt;
void dfs(node *u,int len=0)
{
    if(u->isend)
    {
        printf("%d ",totlen-len+1);
        lcp[++cnt]=minn;
        minn=len;
    }
   //更新部分↓
    if(u->fast)
    {
        find(u);
        dfs(u->fast,len+u->fastlen);
    }
   //更新部分↑
    else
        for(int x=0;x<sigma;x++)
        {
            if(u->next[x])
            {
                if(len<minn)minn=len;
                dfs(u->next[x],len+1);
            }
        }
}

这样复杂度为什么是对的呢?

我数学不好,不会证明,那就来感性理解一下

$ \bullet $ 经过“路径压缩”后的自动机构成一棵树

$ \bullet $ 叶子结点数(=原串后缀数)是 $ O(n) $ 的

$ \bullet $ 非叶子结点数( $ \leq $ 叶子节点数-1)是 $ O(n) $ 的

$ \bullet $ 树的边数是 $ O(n) $ 的

$ \bullet $ 遍历这棵树是 $ O(n) $ 的

应该是这个意思吧QWQ反正跑得飞快QWQ

于是我就这么通过了 UOJ#35 后缀排序 通过记录

第一次写后缀自动机可能写挂了,欢迎dalao来hack

P.S.这种方法貌似在LOJ上会被卡内存...悲剧了...

评论

samzhang
其实这就是某种意义上的后缀树。。。(学名是compact SAM)
zcysky
loj字符集比较大,不好水…… 我以前似乎也这么水过……

发表评论

可以用@mike来提到mike这个用户,mike会被高亮显示。如果你真的想打“@”这个字符,请用“@@”。