算法学习:后缀自动机转后缀树转后缀数组

算法学习:后缀自动机转后缀树转后缀数组

引入

其实这是一篇水文
想要学后缀自动机的话去查2012年noi冬令营陈立杰讲稿
顺便说一句,讲稿上有一些错误,多翻几篇博客加深理解。
今天这里主要要讲的是后缀自动机如何转后缀树和后缀数组

后缀树

这里首先需要提一下后缀树的概念
后缀树其实相当于把所有的后缀插入一颗Trie树
然而时空复杂度都是O(N^2)的
所以我们可以把这可Trie树上的某些边压缩一下
比如这样

这是一颗字符串banans的所有后缀组成的Trie树

这是一颗压缩后的Trie树,也就是后缀树
(图片来源于网络)

从后缀自动机到后缀树

其实操作非常简单,就是把字符串倒序插入后缀自动机,形成的parent树就是后缀树
证明的话,考虑一对parent树上的父子关系。
AAAAxAAAAxBAAx
考虑这样一个字符串
考虑串AAAAx和AAx
显然AAx是AAAAx parent树上的父亲
我们发现,AAx是AAAAx的后缀
我们考虑parent树上从叶子节点到根的路径。
每个节点表示的字符串长度集合[Min(s),Max(s)]越来越小,而且我们还会发现,父亲一定是儿子的后缀。并且儿子的Right集合是父亲的真子集。也就是父亲可能作为别的儿子的后缀
这个时候我们对比后缀树。
仍然是考虑一条从叶子节点到根的路径,我们会发现父亲一定是儿子的前缀。并且一个父亲可能作为很多个儿子的前缀,比如上面例子中的AAx可以作为AAAx,AAAAx,BAAx的后缀。
你瞧,这不是和后缀自动机反着来的么!
那么我们把字符串逆序,是不是意味着所有的前缀变后缀啦?
那么插入后缀自动机中,形成的Parent树不就是一个逆序前的后缀树吗?
这个时候,我们考虑对应关系。
还是原来AAx和AAAAx的例子
我们发现,AAx和AAAAx差了一个“AA”,BAAx和AAx差了一个“B”
这个时候“AA”不会再分叉出去接别的后缀,说明,这条边是可以压缩的。
那么如果我们的字符串已经逆序,AA再原串上就不会再去接别的前缀。
那么AA就可以压缩。
也就是后缀树上的压缩边啦。
那么AA是什么?
我们显然可以在插入后缀自动机的时候记录AAx的位置pos,假设AAx表示的节点为u,AAAAx表示的节点为v,那么AA对应在原串中就是pos+(Max(v)-Max(u))~pos+Max(v)
在后缀树上这就是一条边的标记。因为这条边是可压缩的,所以只要记录这条边的首字符pos+(Max(v)-Max(u))即可。

后缀树转后缀数组

你一定好奇我为什么没有先贴代码。
先不着急,代码可以一起贴。
请你先自己yy一下,如果给你可以后缀树,怎么求后缀数组。
后缀树是一颗Trie树。
是一颗Trie树
Trie树!!!
那不就从根节点开始,按字母字典序dfs搜一下后缀树就可以了吗?
你可能会想:哦,原来这是后缀数组的另外一种求法。不过和原来的算法没差。
真的没差?
原来的方法是倍增法,复杂度O(nlogn)
可是后缀自动机的复杂度是线性的,线性的,线性的!!!
(好吧我得承认它常数跑得太满了,再数据量不大的情况下还是更慢的,比较鸡肋)
现在真的可以贴代码了

代码
#include<iostream>
#include<cstdlib>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<map>
#include<cmath>
using namespace std;
const int N = 1e6 + 10; 
int sa[N], rank[N], s[N], n;
void readchar() {
    char ch = getchar(); int x = 0;
    while(ch < 'a' || ch > 'z') ch = getchar();
    while(ch >= 'a' && ch <= 'z') {s[++x] = ch - 'a'; ch = getchar();}
    n = x;
} 
struct SAM {
    int ch[N][26], pos[N], len[N], fa[N], last, sz;
    bool val[N];
    SAM() {last = ++sz;}
    void Extend(int c, int po) {
        int p = last, np = last = ++sz;
        pos[np] = po; val[np] = true; len[np] = len[p] + 1;
        for(;p && !ch[p][c]; p = fa[p]) ch[p][c] = np;
        if(!p) fa[np] = 1;
        else {
            int q = ch[p][c];
            if(len[q] == len[p] + 1) fa[np] = q;
            else {
                int nq = ++sz; len[nq] = len[p] + 1;
                memcpy(ch[nq], ch[q], sizeof(ch[q]));
                fa[nq] = fa[q]; pos[nq] = pos[q];
                fa[q] = fa[np] = nq;
                for(;ch[p][c] == q; p = fa[p]) ch[p][c] = nq;
            }
        } 
    }
}sam;

struct Suffix_Tree {
    int ch[N][26], pos[N], tp;
    bool val[N];
    void Add(int u, int v, int c) {ch[u][c] = v;}
    void Build() {
        for(int i = 1;i <= sam.sz; ++i) val[i] = sam.val[i], pos[i] = sam.pos[i];
        for(int i = 2;i <= sam.sz; ++i) 
            Add(sam.fa[i], i, s[pos[i] + sam.len[sam.fa[i]]]);
    }
    void Dfs(int u) {
        if(val[u]) sa[rank[pos[u]] = ++tp] = pos[u];
        for(int i = 0 ;i < 26; ++i) 
        if(ch[u][i]) Dfs(ch[u][i]);
    }
}suftree;

int main() {
    readchar();
    for(int i = n; i >= 1; --i) sam.Extend(s[i], i);
    suftree.Build();
    suftree.Dfs(1);
    for(int i = 1;i <= n; ++i) printf("%d ", sa[i]);
    putchar('\n');
    return 0;
}

是的,你没有看错,就算是以博主(我我我!)这样很常(la)数(ji)的代码也只要60行
舒服!

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值