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\) 具体是什么,因此我们可以用线段树合并维护这个东西。

通常会用在询问一个区间子串信息,例题有:P4094P4770

LCT 维护 SAM

我不知道,请查。

例题有:P6292

五:参考资料

本文参考了 Zter 大佬的blog 中的引理结构和部分证明,也推荐大家去阅读该文,因为大佬讲得太详细了。

posted @ 2025-08-21 21:54  XiaoZi_qwq  阅读(7)  评论(0)    收藏  举报