震惊!SAM复杂度竟如此显然!

献给会后缀自动机但不会证时间or空间复杂度的你:)

摘要:本文简单证明了一下SAM的空间复杂度和时间复杂度

(以下n皆为字符串长度

1. 空间复杂度

定理1.1: 后缀自动机的节点数是小于2n的

Proof: 可以参考其他资料,鉴于网上证明比较多且比较简单,这里就不赘述了。

定理1.2: 后缀自动机的转移数是小于3n的

Proof: 首先我们从SAM的DAG上随便一棵抠从起点为根的生成树出来。那么现在转移可以分成两类边,即树边和非树边。因为|树边| = |点数|-1,所以根据定理1.1可以知道这部分是O(n)的,那么我们现在只需要证明非树边也是线性的。

首先我们搞两个集合,第一个是E = {非树边},S = {串的所有后缀}。现在如果我们能找到一个单射函数f: E\rightarrow S就可以证明非树边的个数也是线性的了(而且严格小于等于串长)(不知道单射函数是什么的同学自己搜索一下

那么我们现在来构造这个函数,对于每条非树边u\rightarrow v,我们给他分配的后缀如下定义:

首先我们把这个后缀分成三段

  • 第一段是原点到u的串,因为这是一棵树,所以这个串存在且唯一
  • 第二段就是u\rightarrow v
  • 第三段是从v出发走到任意一个接受状态的节点(这里接受状态的节点定义为任意后缀走到的节点)(注:我们只取任意一个,所以是良定义的(也就是说f(x)不会同时有两个值))(注2:第三段中的边不需要在生成树上)


把这三段拼起来我们就得到了一个后缀

现在我们来证明这个函数是单射的,即f(a)=f(b)\implies a=b

注意到我们给每条非树边分配的后缀有这样一个性质:这条边(u\rightarrow v)是对应后缀在自动机上走到的第一条非树边。那么因为任意一个后缀在自动机上的路径最多只存在一条“第一条非树边”(其实是句废话233),所以如果两条非树边如果被映射到了同一个后缀,他们必定是同一条边,于是我们证明了这确实是个单射的函数,于是非树边数量小于等于后缀的个数(根据单射函数的性质),于是边数就是线性的了。

2. 时间复杂度

先上一个SAM的代码

node* add(node*p,int c){
    node*cur = newnode();
    cur->len = p->len+1;
    while(p &&!p->nxt[c])p->nxt[c] = cur,p = p->fail;
    if(!p){
        cur->fail = root;return cur;
    }
    node*q = p->nxt[c];
    if(q->len == p->len+1){
        cur->fail = q;
    }else{
        node*nq = newnode();
        *nq = *q;
        nq->len = p->len+1;
        q->fail = cur->fail = nq;
        while(p&&p->nxt[c]==q)p->nxt[c] = nq,p = p->fail;
    }
    return cur;
}

复杂度其实就是第四行倒数第四行的两个while所带来的的不确定性了(但我们都知道其实是线性的233)

那么我们现在来分别证明一下这两个while在均摊意义下都是线性的。

如代码所述,我们定义:

cur = 新加入的节点
len = 一个节点中最长串的长度
fail = 一个节点在parent树上的爸爸

第一个while:

我们现在关注

cur->fail->len

这个数(也就是新加入节点的父亲的最长串长度

我们可以发现以下事实:

  • while每循环一次,p->len就至少会减少1(因为向上跳了)。
  • 注意第一次跳是比较特别的,会跳的比较多。(可以撕烤一下为什么)
  • 添加一个字符之后,cur-> fail->len一定等于p->len+1。
  • 添加完cur-> fail->len最多为(之前的cur-> fail->len+1)

由上面几条事实我们可以发现添加完n个字符后,while最多运行了O(n)次(更精确一点是2n次左右)

第二个while:

Update:好像有点问题……等我想清楚再补一下……


总结

本文(不严谨地)证明了后缀自动机的时间和空间复杂度,希望对读者有所启发(顺便改善一下贵圈只用不证的不良风气233(划掉。如有错误,敬请指正。

致谢

附我的SAM模板:

struct SAM{
    static const int maxn =  300010 * 2;
    struct node{
        node*nxt[26],*fail;
        int len;
    };
    
    node*root;int cnt;
    node no[maxn];
    node*newnode(){
        return &no[cnt++];
    }
    SAM(){
        cnt = 0;
        root = newnode();
    }
    
    node* add(node*p,int c){
        node*cur = newnode();
        cur->len = p->len+1;
        while(p &&!p->nxt[c])p->nxt[c] = cur,p = p->fail;
        if(!p){
            cur->fail = root;return cur;
        }
        node*q = p->nxt[c];
        if(q->len == p->len+1){
            cur->fail = q;
        }else{
            node*nq = newnode();
            *nq = *q;
            nq->len = p->len+1;
            q->fail = cur->fail = nq;
            while(p&&p->nxt[c]==q)p->nxt[c] = nq,p = p->fail;
        }
        return cur;
    }

    ll getNumOfDistinctSubstrings(){
        auto ans = 0;
        REP(i,1,cnt)ans+=no[i].len-no[i].fail->len;
        return (ans);
    }
};

编辑于 2019-01-29