SAM学习笔记

SAM学习笔记

这辈子最。。。的就是山姆大叔。
SAM的主要作用就是把一个字符串的后缀放在了Trie上。

对于一个字符串 " a b a b b " "ababb" ,用最朴素的办法构造就是:

而如果用SAM:

简洁了不少啊。
那么这个强大的东西怎么做呢?

前置约定:
对于字符串 u u u |u| 表示它的长度。
对于一个字符集 v v v |v| 表示它的大小。

前置定义:
S S :原串。
s s :原串的字串。
e n d p o s ( s ) endpos(s) :字串 s s 在原串 S S 中出现的位置的右端点的集合。
例如:
S S = " a b a b b " "ababb" s s = " a b " "ab" ,那么 e n d p o s ( s ) endpos(s) ={2,4}。
e n d p o s endpos 等价类: e n d p o s endpos 相同的字串构成的集合。
l o n g e s t ( v ) longest(v) :等价类 v v 中最长的字符串。
s h o r t e s t ( v ) shortest(v) :定义与之类似。

在介绍构造SAM之前,先介绍一些引理。

引理1:
设两个非空字串 u u v v ,且 u v |u|\le|v| ,当且仅当字串 u u 是字串 v v 的后缀。
正确性显然,这里不再证明。

引理2:
对于两个非空子串 u u v v (假设 u v |u|\le|v| )。那么要么 e n d p o s ( v ) e n d p o s ( u ) endpos(v)⊆endpos(u)
要么 e n d p o s ( u ) e n d p o s ( v ) = endpos(u)⋂endpos(v)=∅
正确性同样显然。。。

引理3:
对于一个 e n d p o s endpos 的等价类,其包含的字符串的长度是连续的。
设这个等价类为 v v ,其中所包含的字符串一定是 l o n g e s t ( v ) longest(v) 的后缀,于是显然引理3正确。

引理4:
等价类的个数级别是 O ( n ) O(n) 的。
注意这个很重要。
设这个等价类为 v v ,那么在 l o n g e s t ( v ) longest(v) 前添加一个字符,这个字符串一定不属于该等价类。且如果在 l o n g e s t ( v ) longest(v) 前添加一个字符的 e n d p o s endpos 与在 l o n g e s t ( v ) longest(v) 前添加另一个字符是 e n d p o s endpos 显然是不相交的(根据引理2)。
于是在 l o n g e s t ( v ) longest(v) 前添加字符就相当于对这个字符串的 e n d p o s endpos 进行分割。

而这样的分割次数不会超过等价类本身的大小,于是命题得正。

而这样 e n d p o s endpos 的包含关系形成的树,我们把它叫做 P a r e n t T r e e Parent Tree (这玩意很重要呀呀呀,当时没搞懂,现在懂了其他一目了然)

补充定义: f a v fa_{v} 表示 v v P a r e n t T r e e Parent Tree 上的节点。

引理5:
对于一个等价类 v v l o n g e s t ( f a v ) = s h o r t e s t ( v ) + 1 |longest(fa_{v})|=|shortest(v)|+1
根据引理5, P a r e n t T r e e Parent Tree 的构造方式是在开头添加字符,于是此引理也十分的显然。

L Z C LZC 的话来讲,就是——“毫无征兆的,我们发现SAM的节点可以和 P a r e n t T r e e Parent Tree 的共用”。

就此,世界发生了翻天覆地的改变。

那么,如何构造SAM呢?
(这里要注意SAM的构造是一个在线的过程,一次在字符串后插入字符来改变SAM的形态)

再来一波定义~:设 w h o l e whole 表示在插入字符 x x 之前字符串对应的下标,设 t o t tot 表示字符串的个数。

S t e p 1 Step1
创建新的节点 p p (这里SAM和 P a r e n t T r e e ParentTree 已经合并,需留意),并令 l o n g e s t ( p ) = l o n g e s t ( w h o l e ) + 1 |longest(p)|=|longest(whole)|+1
S t e p 2 Step2
P a r e n t T r e e ParentTree 上,从 w h o l e whole 往上走,如果走到的节点没有 x x 的出边就把此节点和 p p 连一条边。
S t e p 3 Step3
如果走到根了,都没有节点有 x x 的出边,那么令 f a p fa_{p} 为根。
S t e p 4 Step4
否则,设这个结点为 c u r cur ,其 x x 的边通向点 q q
S t e p 5 Step5 :
如果 l o n g e s t ( c u r ) + 1 = l o n g e s t ( q ) |longest(cur)+1|=|longest(q)| ,说明 q q 是新串的后缀。那么 e n d p o s ( n e w ) e n d p o s ( q ) endpos(new)⊆endpos(q) ,令 f a p = q fa_{p}=q
S t e p 6 Step6
否则,新建节点 n x t nxt ,继承 q q 的所有边和 f a q fa_{q}
S t e p 7 Step7
l o n g e s t ( q ) = l o n g e s t ( c u r ) + 1 |longest(q)|=|longest(cur)|+1 , f a p = f a q = n x t fa_{p}=fa_{q}=nxt
S t e p 8 Step8
并且更新原本连 x x 边到 q q 的点,改为连向 n x t nxt
S t e p 9 Step9
更新 w h o l e whole

贴一个代码:

void push(int x)
{
    int cur = whole,p = whole = ++tot;
    sam[p].len = sam[cur].len + 1;
    for(;cur && !sam[cur].ch[x];cur = sam[cur].fa)
        sam[cur].ch[x] = p;
    if(!cur)
        sam[p].fa = 1;
    else
    {
        int q = sam[cur].ch[x];
        if(sam[cur].len + 1 == sam[q].len)
            sam[p].fa = q;
        else
        {
            int nxt = ++tot;
            sam[nxt] = sam[q],sam[nxt].len = sam[cur].len + 1,sam[p].fa = sam[q].fa = nxt;
            for(;cur && sam[cur].ch[x] == q;cur = sam[cur].fa)
                sam[cur].ch[x] = nxt;
        }
    }
}

猜你喜欢

转载自blog.csdn.net/INnovate2030/article/details/102875248
SAM