SAM 学习笔记
初步认识
SAM 是一个有限状态自动机。
可接受状态为字符串的所有本质不同子串,转移是字符。
SAM 是满足上述条件的最小 DFA。
一:结束位置 \(endpos\) 相关引理
约定 \(1\)
我们定义:对于一个字符串 \(T\),\(endpos(T)\) 表示 \(T\) 在 \(S\) 中所有出现的结束位置的集合; \(|S|\) 表示 \(S\) 的长度;\(S[l:r]\) 表示 \(S\) 中第 \(l\) 个字符到第 \(r\) 个字符构成的字符串。
引理 \(1\)
内容:对于 \(S\) 的任意两个子串 \(s_1,s_2\),令 \(|s_1|\ge |s_2|\)。“ \(s_1\) 和 \(s_2\) 的 \(endpos\) 相同”是“ \(s_2\) 每次在 \(S\) 中的出现都是以 \(s_1\) 的后缀的形式”的充要条件。
证明:这个必要性显然成立,充分性证明可以考虑对于每一个位置 \(x \in endpos(s_1)\),都有 \(s1=S[x-|s_1|+1:x],s_2=S[x-|s_2|+1:x]\),因为 \(|s_1|>|s_2|\),所以 \(s_2\) 是 \(s_1\) 的后缀。
举例:\(S=aabaabaabaa,s_1=aab,s2=ab\) 和 \(S=abcbabcba,s_1=abcb,s_2=cb\)。
Q.E.D
引理 \(2\)
内容:对于 \(S\) 的任意两个子串 \(s_1,s_2\),令 \(|s_1|\ge |s_2|\)。若 \(s_2\) 是 \(s_1\) 的后缀,那么 \(endpos(s_1) \in endpos(s_2)\);若 \(s_2\) 不是 \(s_1\) 的后缀,那么 \(endpos(s_2) \cap endpos(s_1)=\empty\)。
证明:
先证明是后缀的情况:首先,\(s_2\) 在 \(endpos(s_1)\) 中的每一个位置都以 \(s_1\) 的后缀的形式出现;然后,因为 \(|s_2| < |s_1|\) ,那么 \(s_2\) 就可能可以匹配更多的位置,比如 \(S=aabaabab,s_1=aab,s_2=ab\)。
再证明不是后缀的情况:考虑反证法,对于每一个位置 \(x \in endpos(s_1)\)。那么有 \(s_1=S[x-|s_1|+1:x]\),如果同时有 \(x \in endpos(s_2)\),那么有 \(s_2=S[x-|s_2|+1:x]\),也就是 \(s_2\) 是 \(s_1\) 的后缀,矛盾!因此结论得证。
Q.E.D.
约定 \(2\)
我们定义:一个 \(endpos\) 等价类是所有 \(endpos\) 相同的字符串构成的集合。
引理 \(3\)
内容:一个 \(endpos\) 等价类中不会包含两个长度相同且本质不同的字符串。
证明:
考虑反证法。假设存在满足上述条件的两个字符串 \(s_1,s_2\),由引理 \(1\) 可知:\(s_1\) 每次在字符串中都以 \(s_2\) 的后缀的形式出现,且 \(s_2\) 每次在字符串中以 \(s_1\) 的后缀的形式出现,也就是 \(s_1\) 和 \(s_2\) 互为后缀,因此 \(s_1=s_2\),矛盾!因此结论得证。
Q.E.D.
引理 \(4\)
内容:考虑一个 \(endpos\) 等价类 \(A\),对于 \(s_1,s_2 \in A(|s_1| \ge |s_2|)\),要么 \(s_1=s_2\),要么 \(s_2\) 是 \(s_1\) 的真后缀。
证明:
由引理 \(3\) 得:当 \(|s_1|=|s_2|\) 时,\(s_1=s_2\)。
由引理 \(1\) 得:当 \(|s_1| > |s_2|\) 时,\(s_2\) 在字符串中的每次出现都是以 \(s_1\) 的后缀的形式,因此 \(s_2\) 是 \(s_1\) 的后缀。
Q.E.D.
引理 \(5\)
内容:考虑一个 \(endpos\) 等价类 \(A\),设 \(l,r\) 分别为 \(A\) 中的最长字符串 \(s_1\) 和最短字符串 \(s_2\) 的长度。那么 \(A\) 中所有字符串长度的并集为 \([l,r]\) 中所有整数。(即 \(endpos\) 等价类中长度连续)
证明:
由引理 \(4\) 可得: \(A\) 中所有字符串都是 \(s_1\) 的后缀,因此考虑对于集合 \(Q=\{ t | t \text{ is the suffix of } s_1,|t| \in [l,r]\}\),我们只需要证明 \(Q=A\),也就是证明 \(Q \subseteq A,A\subseteq Q\)。
先证明 \(A \subseteq Q\):这是显然的,因为 \(A\) 中所有字符串都是 \(s_1\) 的后缀,且长度在 \([l,r]\) 之间。
再证明 \(Q \subseteq A\):考虑反证法,如果存在一个字符串 \(s' \notin A\),我们设 \(s' \in A'\),由引理 \(2\) 可以得:\(endpos(s_2) \in endpos(s') \in endpos(s_1)\) 的同时 \(endpos(s_1)=endpos(s_2)\),即 \(endpos(s‘)=endpos(s_1)\) ,说明 \(s’ \in A\) ,矛盾!因此 \(Q \subseteq A\)。
总结
由引理 \(1\) 至引理 \(5\) 我们可以知道,一个 \(endpos\) 等价类对应若干字符串,且这些字符串长度连续。
在 SAM 中,一个节点对应一个 \(endpos\) 等价类,这样我们就不重不漏地把所有本质不同字符串对应到 SAM 上的节点。
二:后缀链接 \(link\) 相关引理
定义 \(4\)
对于一个字符串 \(s\),我们找到它最长的后缀 \(t\) ,其中 \(t\) 满足 \(endpos(t) \neq endpos(s)\)。设 \(endpos(s)=A,endpos(t)=B\),我们对于 \(link(A)=B\)。
引理 \(6\)
内容:对于一个 \(endpos\) 等价类 \(A\),我们定义 \(s\) 为 \(A\) 中的最短字符串。$A \subsetneq link(A) $,且 \(link(A)\) 中的最长字符串的长度为 \(|s|-1\)。
证明:
先证明第一部分:因为 \(link(A)\) 中的每一个字符串都是 \(A\) 中任意一个字符串的后缀,由引理 \(2\) 可知:\(A \subset link(A)\);根据 \(link\) 的定义,我们可以知道 \(A \neq link(A)\),结论得证;
再证明第二部分:由 \(link\) 的定义可以简单证明。
引理 \(7\)
内容:所有 \(link\) 构成一个内向树(即 Parent 树)。
证明:
由后缀链接的定义可知,从任意一个节点 \(A\) 出发,每一次走 \(link\) 访问到的节点中的每一个字符串都是 \(A\) 中最长字符串 \(s\) 的后缀,且长度严格递减,即最后一定会访问到空串。
Q.E.D
总结
\(link\) 是 SAM 中将各个 \(endpos\) 等价类(节点)链接起来的重要信息。
三:构造
构造的核心目的是处理怎么加入一个节点。
一个节点显然要存三个东西(当然因题目不同可能会增加一些东西):
-
最长字符串的长度
-
后缀链接
-
转移
这里给出一份代码:
int n,tr[N][26],ink[N]={-1},len[N],lst,tot,cnt[N];
char s[N];
void insert(int c){
int cur=++tot,p=lst;
//首先,lst是表示插入前整个字符串的节点
//很明显,插入一个新字符后,长度+1
len[cur]=len[lst]+1;
//维护转移:
//lst->cur 是显然的
//因为 link(lst) 是 lst 的后缀,那么也存在 link(lst) -> cur
while(p!=-1 && !tr[p][c])
tr[p][c]=cur,p=ink[p];
//接下来在找 link(cur)
if(p==-1)//如果维护转移的时候没有冲突,那么说明 c 是一个全新的字符
ink[cur]=0;
else{
//如果 tr[p][c] 已经存在,那么我们要看 tr[p][c] 和 cur 是否可以构成后缀链接关系
int q=tr[p][c];
if(len[p]+1==len[q])//满足 link 的定义,那么就赋值
ink[cur]=q;
else{
//不满足,那就将 q 分成两个节点,其中一个节点满足条件
int clone=++tot;
len[clone]=len[p]+1;
ink[clone]=ink[q];
For(i,0,25) tr[clone][i]=tr[q][i];
//因为 clone 满足 len[clone]=len[p]+1,因此有 tr[p][c]=clone
//类似的,link(tr[p][c]) -> clone
while(p!=-1 && tr[p][c]==q)
tr[p][c]=clone,p=ink[p];
ink[q]=ink[cur]=clone;//维护 cur 和 q 的 link
}
}
lst=cur;
}
/*
关于时间复杂度的感性证明:
我们注意到:如果没有跳link的操作,那么时间复杂度一定为线性。
那么我们只需要分析跳link的时间复杂度即可
我们定义 dep 为当前可以跳 link 的次数。
每一次加入节点时,dep加1
每一次跳link时,dep减1。
因为最多加入 |s| 个节点,所以 dep 只会增加 |s| 次。
因此时间复杂度线性。
*/
四:应用
简单的应用大家应该都会,不会请看其他blog,qwq。
线段树合并维护endpos
为什么要这么做呢?因为有的时候,我们不仅想要知道 \(endpos\) 的大小,我还想知道 \(endpos\) 具体是什么,因此我们可以用线段树合并维护这个东西。
通常会用在询问一个区间子串信息,例题有:P4094,P4770。
LCT 维护 SAM
我不知道,请查。
例题有:P6292。
五:参考资料
本文参考了 Zter 大佬的blog 中的引理结构和部分证明,也推荐大家去阅读该文,因为大佬讲得太详细了。