SAM学习笔记
这辈子最。。。的就是山姆大叔。
SAM的主要作用就是把一个字符串的后缀放在了Trie上。
对于一个字符串 ,用最朴素的办法构造就是:
而如果用SAM:
简洁了不少啊。
那么这个强大的东西怎么做呢?
前置约定:
对于字符串
,
表示它的长度。
对于一个字符集
,
表示它的大小。
前置定义:
:原串。
:原串的字串。
:字串
在原串
中出现的位置的右端点的集合。
例如:
=
,
=
,那么
={2,4}。
等价类:
相同的字串构成的集合。
:等价类
中最长的字符串。
:定义与之类似。
在介绍构造SAM之前,先介绍一些引理。
引理1:
设两个非空字串
和
,且
,当且仅当字串
是字串
的后缀。
正确性显然,这里不再证明。
引理2:
对于两个非空子串
和
(假设
)。那么要么
,
要么
正确性同样显然。。。
引理3:
对于一个
的等价类,其包含的字符串的长度是连续的。
设这个等价类为
,其中所包含的字符串一定是
的后缀,于是显然引理3正确。
引理4:
等价类的个数级别是
的。
注意这个很重要。
设这个等价类为
,那么在
前添加一个字符,这个字符串一定不属于该等价类。且如果在
前添加一个字符的
与在
前添加另一个字符是
显然是不相交的(根据引理2)。
于是在
前添加字符就相当于对这个字符串的
进行分割。
而这样的分割次数不会超过等价类本身的大小,于是命题得正。
而这样 的包含关系形成的树,我们把它叫做 (这玩意很重要呀呀呀,当时没搞懂,现在懂了其他一目了然)
补充定义: 表示 在 上的节点。
引理5:
对于一个等价类
,
。
根据引理5,
的构造方式是在开头添加字符,于是此引理也十分的显然。
用 的话来讲,就是——“毫无征兆的,我们发现SAM的节点可以和 的共用”。
就此,世界发生了翻天覆地的改变。
那么,如何构造SAM呢?
(这里要注意SAM的构造是一个在线的过程,一次在字符串后插入字符来改变SAM的形态)
再来一波定义~:设 表示在插入字符 之前字符串对应的下标,设 表示字符串的个数。
:
创建新的节点
(这里SAM和
已经合并,需留意),并令
。
:
在
上,从
往上走,如果走到的节点没有
的出边就把此节点和
连一条边。
:
如果走到根了,都没有节点有
的出边,那么令
为根。
:
否则,设这个结点为
,其
的边通向点
。
:
如果
,说明
是新串的后缀。那么
,令
。
:
否则,新建节点
,继承
的所有边和
。
:
令
,
。
:
并且更新原本连
边到
的点,改为连向
。
:
更新
。
贴一个代码:
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;
}
}
}