SAM & 广义 SAM & SA 学习笔记

SAM

定理

SAM 由 parent 树与一张 DAG 构成,他们共用点集。

\(endpos(s)\) 表示 \(s\) 出现的所有位置上最后一个字符所处位置的集合。

SAM 中 DAG 上每条路径对应原串上的一个子串,一个子串也与其对应。

在 SAM 的 DAG 上到达一个点的所有子串的 endpos 相同。

一个节点上储存的最长的串为 \(len_i\)

在 parent 树上点 \(u\) 的父亲节点是与 \(u\) endpos 不相同的 \(len_{u}\) 的最长真后缀。

不难发现点 \(u\) 的 endpos 包含所有满足 \(v \in son_{u}\)\(v\) 的 endpos,不难发现一个点 \(u\) 存储的串就是所有长度小于等于自己并大于其父节点的所有后缀。

构建

增量法

已知串 \(S\) 的 SAM,如何构造 \(S+c\) 的 SAM?

找到一个节点 \(u\) 使得 \(len_{u} = |S|\)

考虑在 parent 树上 \(u\) 的所有祖先包含 \(S\) 的所有真后缀。

在这条路径上显然应当是满足所有深度大于 \(t\) 的点没有 \(c\) 这条出边。

新建一个点 \(p\) 使得其 \(len_{p} = len_{u} + 1\) 等于说是包括了 \(c\)

让所有没有 \(c\) 这条出边的点向 \(p\) 连一条 \(c\) 的边。

到此为止 DAG 的维护就完成了。

接下来考虑维护 parent 树与 endpos。

大力分讨。

  1. 假设所有点都向 \(p\) 连边,就说明 \(c\) 根本就没出现过,那么其所有后缀的 endpos 都是 \(len_{p}\) 那么将 \(p\) 在 parent 树上的父亲改为根即可。

  2. 假设存在点向 \(p\) 连边,找到第一个存在向 \(c\) 的边的点 \(x\),假设其向 \(c\) 的边指向 \(q\),假若 \(len_{x} + 1 = len_{q}\),直接令 \(fa_{p} = q\)。因为 \(p\) 最长只能跳这来。

  3. 假若 \(len_{x} + 1 \not = len_{q}\) 那就有点麻烦,那么此时需要分裂 \(q\),假设你分裂出了新点 \(nq\),直接令 \(SAM_{nq} = SAM_{q}\),然后手动将 \(SAM_{nq,len} = SAM_{x,len} + 1\),然后让 \(SAM_{q,fa} = nq\),然后让 \(SAM_{p,fa} = nq\)。但是此时 DAG 的性质又似了,我们需要把所有 \(x\) 的祖先当中原本指向 \(q\) 的点全部指向 \(nq\)

至此你发现 parent 树和 endpos 全部维护好了。

复杂度是神奇的线性乘字符集。

不难发现一个点的最长串一定都这个点上不会被分类,那么每次插入字符新建的点一定代表一个前缀,跳 parent 树即可跳出子串。

广义 SAM

其实差不多。

有点小区别,当你求所有串的公共子串时,需要把每个子串的公共子串都遍历一遍,自己遍历过的点自己不再遍历但是其他点仍需要遍历,复杂度是 \(O(len \sqrt len)\) 的。

考虑一个串的遍历复杂度是 \(O(len^2,\sum |S|)\) 那么均摊下来一个字符的复杂度是 \(O(\min(len,\frac{\sum |S|}{len}) \leq O(\sqrt{\sum |S|})\),所以总复杂度是 \(O(\sum |S| \sqrt{\sum |S|})\)

SA

后缀数组 \(rk_{i}\) 表示字典序第 \(i\) 的后缀为 \([rk_{i},n]\)

考虑怎么构造。

有一个无脑的 \(O(n \log^2 n)\) 做法,不提了。

考虑倍增。

考虑把每个 \([i,i+2^k]\) 拿出来排序,因为上一次已经排好了,所以用两个关键字做基数排序即可。

height 数组

\(lcp(i,j)\) 表示 \([rk_i,n]\)\([rk_j,n]\) 的最长公共前缀,有 \(lca(i,j) = \min(lca(i,j),lca(j,k))\),假若我们处理出 \(height_{i} = lcp(i,i-1)\) 就能把 \(lca(i,j)\) 变成 RMQ 问题。

考虑求出 \(height\) 数组。这里给一个新数组 \(h\) 数组。

考虑 \(height_{i} = h_{rk{i}}\)

\(h_{i+1} \geq h_{i} - 1\) 然后考虑去暴力扩展。

大不了全部扩展完,是 \(O(n)\) 的。

板子

广义 SAM

struct SAM{
	int len,fa;
	int son[26];
}nd[maxn]; 
int tot,lst;
void ins(char c){
    if(nd[lst].son[c-'a']!=0){
    	int q=nd[lst].son[c-'a'],v=lst;
        if(nd[q].len==nd[v].len+1){
        	lst=q;
        	return ;	
		}
        int nq=++tot;
        lst=nq;
        nd[nq]=nd[q];
        nd[nq].len=nd[v].len+1;
        nd[q].fa=nq;
        while(v!=0&&nd[v].son[c-'a']==q) nd[v].son[c-'a']=nq,v=nd[v].fa;
        return ;
    }
    int u=++tot,v=lst;
	nd[u].len=nd[lst].len+1;
  	lst=u;
	while(v!=0&&nd[v].son[c-'a']==0) nd[v].son[c-'a']=u,v=nd[v].fa;
	if(v==0){
        nd[u].fa=1;
        return ;
    }else{
        int q=nd[v].son[c-'a'];
        if(nd[v].len+1==nd[q].len){
            nd[u].fa=q;
            return ;
        }else{
            int nq=++tot;
            nd[nq]=nd[q];
            nd[nq].len=nd[v].len+1;
            nd[u].fa=nq;
            nd[q].fa=nq;
            while(v!=0&&nd[v].son[c-'a']==q) nd[v].son[c-'a']=nq,v=nd[v].fa;
            return ;
        }
    }
}
int rt;
void insert(string s){
    lst=rt;
    for(char x:s) ins(x);
}
posted @ 2024-01-30 23:18  ChiFAN鸭  阅读(25)  评论(0编辑  收藏  举报