ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

SAM复杂度证明

2022-03-08 13:34:10  阅读:164  来源: 互联网

标签:那么 SAM 后缀 复杂度 证明 int 这个 endpos now


关于$SAM$的复杂度证明(大部分是对博客的我自己的理解和看法)

这部分是我的回忆,可省略

先回忆一下$SAM$

我所理解的$SAM$,首先扒一张图

初始串$aabbabd$

首先发现,下图里的$S->9$的一条直线是$aabbabd$是原串

那么从这里我们就可以看到$endpos$关系了,和$AC$自动机不同的是

发现一些子串结尾是相同的,那么就可以共用一个节点,那么从起点到这个点能表示的所有子串的$endpos$相同,那么显然可以共用这个点,这就是空间上的能省就省

又因为这个$SAM$是为了表示所有的子串或者后缀,那么$endpos$相同的话,也就是说在这个状态结束之后都会在这个点继续向后延伸

先粗糙的理解一下,那我们是不是可以理解为,我们要插入一个后缀,那么需要节省空间吧,那么如果一个以他为结尾的点,在多个子串里经过,那么就可以多次使用这个点

那么就拿下图的$4$举例,前面有三个满足条件(下面解释)的后缀有三个,那么这个节点可以被使用三次在三个后缀里

这个$4$节点表示的$endpos$也仅仅只是只是$endpos=4$

还是举例$ab$这个后缀为什么没在$S->7$的某条路径上,其实本应该出现的,发现这时$ab$在$S->8$的路径上,这个被分出来了,为何,因为这时$endpos(ab)!=7,$应该出现是因为他的$endpos$有$7$,没有出现是因为这是一个新的类型,如果归成一类的话,就无法满足经过这个之后统一在这个点出去了,那么只能自成一家

虽然自成一家了,也不是毫无关系,毕竟$endpos$集合有重复的部分,那么显然的,这个$endpos$集合是一个有序的,就是递增的,那么在一个串是另一个串后缀的时候,越短的串的$endpos$越大,而且大的集合必然包含小的集合,那么这个东西就是可以通过一个指向关系来确定了

转移边也是相当于$AC$自动机的转移边,就是这个$endpos$集合能到达的下个$endpos$集合,上文说了,我们把所有仅在这个点结束的统一放一起,那么可以在这里统一出发向能到达的所有$endpos$去转移

上面说的这些,到这里汇总一下,思考这个东西是如何构造的

找出所有后缀一个个插入$ \xcancel{\huge NO} $

增量法构造$\checkmark $

在增量的过程中思考一下复杂度

首先明确我们在自动机维护什么,修改时改变什么

维护$len_{max},len_{min},trans,link$

这个东西肯定在遍历$SAM$没啥用,那么我们可以用这几个东西快速找到插入点

构造自动机,假设我们目前插入$S_k$

我们建完了前面的自动机,需要加一个字符,也就是需要表示的字符多了$k-1$个,就是所有包含最后一个字符的子串

首先在后缀自动机上多一个节点,表示$endpos=k$,显然的,从大到小的所有新增字符串,首先,最大的$endpoz=k$,那么小的字符串的$endpos$可能不仅仅是$k$,可能在前面也出现了,在自动机上的体现就是一个节点的$tran[now][s[k]]!=0$,也就是说这个后缀曾经被表示了,这个时候看看上面的需要改变的部分,首先这个被表示的后缀的$endpos$发生了变化,多了一个位置,那么这个状态其他的如果没变的话,就需要把这个状态分开了,上面证明,越长的串$endpos$越小,那么会分成两部分,变的和不变的,注意$!$这个时候插入一个串要么增加一个节点,要么不变,不会再增加更多节点了,那么我们需要解决的仅仅是在跳跃找的时候的复杂度了(说实话,这个我没仔细看过...)

时间复杂度和你每次添加新字符多的状态数和需要跳几次有关

上文证明了,状态数$O(2\times n)$

放一份代码

//回顾 
//Link一个字符串所有后缀变换时的链接位置 
//trans是增加一个字符之后到的状态
//一个状态只有有好多串,但是转移边上只有一个字符
// 一个边上多个字符是后缀树
//其实SAM上的边表示状态转移
//由于状态之间相同的合并了,所以空间较优 
#include<bits/stdc++.h>
#define MAXN 3100000
using namespace std;
string s;
int cnt[MAXN],tr[MAXN][30],len[MAXN],fa[MAXN],sz[MAXN];
int last=1,tot=1;
int ans=0;
vector<int>road[MAXN];
void add(int c)
{
     int p=last; //上一次增量的新点的位置
     int now;
     now=last=++tot; //更新新建节点位置
     cnt[now]=1;
     len[now]=len[p]+1; //当前节点的maxlen,如果不分裂,那么maxlen必然是上一个长度+1
     for(;p&&!tr[p][c];p=fa[p]) tr[p][c]=now;
     //更新trans,如果没有这个状态,那么就变成当前状态
     if(!p) fa[now]=1; //fa用来跳link
     //Link向前跳,串越来越小,状态越来越多 
     //如果!p 证明这个串的所有后缀(新后缀)的(endpos)一样那么link直接指到1
     else
     {
            //开始拆点
           int q=tr[p][c];
           //这时候有一个转移状态
           //相当于原来有ababc的转移,那么abab加c已经有转移状态
           if(len[q]==len[p]+1) fa[now]=q;
           //如果发现这个点正好是last+1的maxlen,那么就相当于
           //有了一个转移的点,那么直接把now的Link指过去即可
           else
           {
                  //大力拆点
               int spilt=++tot;
               for(int i=0;i<=25;i++)
               {
                      //要拆点,拆成一个maxlen大的x和一个小的y
                   //由于越小放前面
                   //那么小的先和原来的相连,进行信息复制
                   //显然的,一个状态加一个字符都到一个新状态
                   tr[spilt][i]=tr[q][i]; 
               } 
               fa[spilt]=fa[q];
               //spilt是小的,继承状态 
               len[spilt]=len[p]+1;
               //发现现在其实只需要改Link
               //trans的作用是转移存在就不用管了
               //由于这个时候多了一个位置
               //那么从断点的位置Link必定改变
               //那么更改Link就可以了
               //发现其实尽管有这个转移边,但是状态不一样
               //那么就可以两个都连想spilt
               fa[q]=fa[now]=spilt;
               for(;p&&tr[p][c]==q;p=fa[p]) tr[p][c]=spilt;
               //更改前面的转移边 
           } 
     } 
}
void dfs(int x)
{
     for(int i=0;i<road[x].size();i++)
     {
          int y=road[x][i];
          dfs(y);
          cnt[x]+=cnt[y];
     }
     if(cnt[x]!=1) ans=max(ans,cnt[x]*len[x]);
//     cout<<cnt[x]*len[x]<<endl;
} 
int main()
{
    cin>>s;
    int len=s.size();
    s=' '+s;
    for(int i=1;i<=len;i++)
    {
        add(s[i]-'a');
    }
    for(int i=2;i<=tot;i++)
    {
        road[fa[i]].push_back(i);
        //后缀树 
    }
    dfs(1);
    cout<<ans;
}

 

 

看一下$add$函数

其余的都是$O(1),$除了几个循环,那么看这几个循环

第一个循环

for(;p&&!tr[p][c];p=fa[p]) tr[p][c]=now;

 

每个点都有一个$trans$,每个点最多被赋值一次,均摊下来,每个点只被操作一次,点数是状态数,$O(|S|)$

其实你更改的是连续的一部分,每次都会改连续的一段,绝对不会出现一段被多次经过情况,那么每个点至多被经过一次

第二个循环

for(int i=0;i<=25;i++)
{
//要拆点,拆成一个maxlen大的x和一个小的y
//由于越小放前面
//那么小的先和原来的相连,进行信息复制
//显然的,一个状态加一个字符都到一个新状态
tr[spilt][i]=tr[q][i];
}

 

每次至多多一个状态去复制,那么复杂度是$O(25|S|)$,尽管是个$25$的常数...

第三个循环

for(;p&&tr[p][c]==q;p=fa[p]) tr[p][c]=spilt;
//更改前面的转移边

 

这个貌似好麻烦...

首先这个东西是更一下转移边,现在不是分成两部分了吗,一个是没有变化的部分,一个是变化的部分

那么改变$tran$的是能到这个旧的状态的需要把这些转移搞到旧状态上(新开的$split$点)来,相当于复制一遍

我们改变的是所有与旧状态相连的边,那么我们考虑接下来的所有这个操作,本质是把这个点裂开,那 么考虑下面的裂开操作,是因为这个点的$endpos$变化,又证明,越短的串越容易改变,那么考虑变化的肯定不是这个点,而是这次分裂操作得到的另外一个点,感性理解一下,每次只裂开一个点,总不能把容易变得放一边,把不容易变得裂开吧,也就是说,每个节点至多被遍历到一次,每个节点的连边最多被遍历到一次,那么复杂度就和边数有关了

边数也就是$tran$的数量,在整个$SAM$的数目是$O(|S|)$的(怎么我看到的证明都不是人话啊...)

还是考虑搞一个生成树,目前的$SAM$并不完整

$trans$的作用是能遍历到所有子串,从终止节点往回跑所有以它为结尾的子串(倒着跑),发现不能表示出来了就加边,而且考虑加边的话是因为$endpos$不一样(一样的话就是能顺着跑下来了),那么最多加$endpos$集合大小条边

那么对于每个终止节点都跑一遍,最多加了$\sum(|endpos|)$(就是$endpos$集合大小的和),增加了$O(n)$个

那么$trans$也是$O(n)$了

从$3.7,21:00$开始写,中间有一场模拟赛,直到$3.8,13:05$写完

对着一个证明卡了半天~,像个zz,hhh

标签:那么,SAM,后缀,复杂度,证明,int,这个,endpos,now
来源: https://www.cnblogs.com/Eternal-Battle/p/15979965.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有