W
e
l
c
o
m
e
: )

[字符串] SAM 学习笔记

SAM 的初始化

约定

本文中 “\(A\) 是 \(B\) 的后缀” 成立条件为 \(|A|<|B|\) 且 \(\forall i\le |A|, A_{|A|-i+1}=B_{|B|-i+1}\)。

性质

SAM 的结点维护的是所有出现位置集合(我们用出现位置的末尾来表示集合,即 endpos)相等的串。

SAM 分为实点/虚点,实点表示 原串 的本身所有后缀点,虚点则为辅助点,用于表示表示原串所有的子串。

  1. 两个串的 endpos 之间只有包含或不交的关系。

  2. 若两个串的 endpos 有交(即一个为另一个的子集),那么小串是大串的后缀。

  3. 所有 endpos 等价类的串长是一个连续的区间,且不存在重复元素。

SAM 的 parent 树根据性质 2 建立(这显然构成一棵树关系,且父亲的所有串均为儿子的串后缀,因为 endpos 越大串长越小),同时由于性质 3,若我们设某个结点 endpos 等价类串长最大的为 \(r_{x}\),则其 endpos 等价类的串长区间为 \((r_{fa_{x}}, r_{x}]\),不妨设 endpos 等价类中最长的串为 \(Mx_{x}\) 且串长为 \(len\),最短的串为 \(Mn_{x}\)。

而 SAM 的边是结点与结点之间的转移边,根到某个结点的所有链上形成的字符串的 endpos 相等,为该点的 endpos。

构造

考虑增量构造 SAM,类似于 AC 自动机,新加入一个字符 \(ch_{i}\) 时,新建一个点 cur,其能表示的最长子串就是当前前缀 \([1, i]\),其长度 \([1, i-1]\) 的 \(len+1\)(我们用 lst 表示上一个前缀所在的结点),我们从 \(lst\) 向上跳(这样能遍历 \([1, i-1]\) 的所有后缀,插入 \(ch\) 这个字符),连 \(lst \to^{ch} cur\)(如果没有这条边那么 \(root \to lst \to^{ch} cur\) 这个串的 endpos 就与 \([1, i]\) 的 endpos 相同,即 \(\{i\}\)),直到跳到的点本身有 \(ch\) 的这条边或根节点,设停下的点为 \(p\)。

若跳到了根,则直接连 \(p \to^{ch} cur\),结束构造。

否则,检查 \(q=p\to^{to} ch\) 的 endpos 大小是否为 1(即 \(p.len+1=q.len\)),如果是,那么这说明 \(root \to q\) 这个串是 \([1, i]\) 的一个后缀,根据定义,我们在 parent 树上连边 \((q, cur)\) 即可结束构造。

若不是,则 \(Mx_p\to^{to} ch\) 这个串是 endpos 变化的串中最长的串(最短的串就是 \(Mx_{fa_q}\to^{to} ch\),所以 parent 树的父亲不需要修改),我们设置一个新结点 \(nex\),保留 \(q\) 的所有转移边(它要替代 SAM 中 \(q\) 的位置,因此向上找所有连向 \(q\) 的 SAM 转移边,把指向点改为 \(nex\)),注意到 \(nex\) 的 endpos 是在 \(q\) 的基础上多一个位置,且 根据性质 2 仍有 \(nex\) 的串,那么根据定义 \(nex\) 是 \(q\) 的新 parent,同理也是 \(cur\) 的 parent。

每次构造完后记得更新 \(lst=cur\)。

namespace SAM{
	const int Nc=N<<1;//SAM 实际点数卡满 2n-1
	const int Mc=(N<<1)+N;//SAM 实际边数卡满 3n-4
	int ndc, lst;
	struct node{int fa, len, nxt[26];}sam[Nc];
	int clear(int x){sam[x]=sam[0], siz[x]=0;return x;}
	void remake(){clear(ndc=lst=1);}
	void insert(char c){
		int cur=clear(++ndc), p=lst, cc=c-'a';
		sam[cur].len=sam[lst].len+1;
		for(; p&&!sam[p].nxt[cc]; p=sam[p].fa)
			sam[p].nxt[cc]=cur;
		int q=sam[p].nxt[cc];
		if(!q) sam[cur].fa=1;
		else if(sam[q].len==sam[p].len+1) sam[cur].fa=q;
		else{
			int nex=clear(++ndc);sam[nex]=sam[q], sam[nex].len=sam[p].len+1;
			for(; p&&sam[p].nxt[cc]==q; p=sam[p].fa) 
				sam[p].nxt[cc]=nex;
			sam[cur].fa=sam[q].fa=nex;
		}
		lst=cur;
	}
} 

广义 SAM

在每次插入新的串的时候将 \(lst\) 重新设为 1,需要特判 \(lst \to^{ch}\) 的边是否存在。

若存在,正常判断 \(q=lst \to^{ch}\) 的等价类是否可以继续分裂,如果不行,相当于插入这个字符直接走到了 \(q\)。

如果行,我们要将新插入的字符与之前的区分开来,新建点 \(cur\),取代 SAM 和 parent 树上的 \(q\),最后将 \(cur\) 设为 \(q\) 的父亲。

	void insert(int cc){
		if(sam[lst].nxt[cc]){
			int p=lst, q=sam[lst].nxt[cc];
			if(sam[p].len+1==sam[q].len) return (void)(lst=q);
			else{
				int cur=clear(++ndc);sam[cur]=sam[q];
				sam[cur].len=sam[p].len+1;
				for(; p&&sam[p].nxt[cc]==q; p=sam[p].fa)
					sam[p].nxt[cc]=cur;
				sam[cur].fa=sam[q].fa, sam[q].fa=cur;
				return (void)(lst=cur);
			}
		}
		int cur=clear(++ndc), p=lst;siz[cur]=1;
		sam[cur].len=sam[lst].len+1;
		for(; p&&!sam[p].nxt[cc]; p=sam[p].fa)
			sam[p].nxt[cc]=cur;
		int q=sam[p].nxt[cc];
		if(!q) sam[cur].fa=1;
		else if(sam[q].len==sam[p].len+1) sam[cur].fa=q;
		else{
			int nex=clear(++ndc);sam[nex]=sam[q], sam[nex].len=sam[p].len+1;
			for(; p&&sam[p].nxt[cc]==q; p=sam[p].fa) 
				sam[p].nxt[cc]=nex;
			sam[cur].fa=sam[q].fa=nex;
		}
		return (void)(lst=cur);
	}

SAM 的应用

匹配子串

SAM 本质是一个对子串建立的自动机,因此直接暴力匹配即可,单词匹配复杂度为文本串长。

定位子串

实际上,当文本串为原串的子串 \(S_{[l,r]}\) 时,我们可以更快速的找到匹配最后会走到的结点。

考虑 \([1, r]\) 的定位结点 \(pos_{r}\),这是插入中可以维护的(实际上就是 \(cur\)),我们在 \(parent\) 树上向上跳,根据性质 2 和 3,\(x\) 的结点包含的为所有右端点为 \(r\),左端点在 \([r-{len_{x}}+1, r-len_{fa_{x}})\) 的子串,所以当 \(len_{x}\ge r-l+1\) 时,\([l, r]\) 的定位结点至少为 \(x\) 或 \(x\) 的祖先。所以我们要找的就是 \(pos_{r}\) 的祖先中,深度最小的且 \(len\ge r-l+1\) 的点,由于 \(len\) 随着深度变浅变小,所以直接倍增跳即可做到 \(O(\log n)\) 定位。

endpos 与 子串出现次数

注意到在 SAM 的 build 中,我们实际上并没有维护 endpos 具体是什么。

实际上,根据 parent 树的性质,我们可以自底向上线段树合并得到每个串的 endpos,那么考虑初始的 endpos,显然 \(pos_{r}\) 可以是最浅的 endpos 包含 \(r\) 的结点,那么只需要在这个位置单点增加 \(r\) 这个位置即可。

注意我们可能需要在线查 endpos,所以线段树合并时要新建结点来表示区分两棵树。

子串定位后的出现次数就是 \(|\text{endpos}|\)。

namespace SAM{
	const int Nc=N<<1;
	const int Mc=(N<<1)+N;
	int ndc, lst, siz[Nc], f[Nc][20], pos[N];
	struct node{int fa, len, nxt[26];}sam[Nc];
	int clear(int x){sam[x]=sam[0], siz[x]=0;return x;}
	void remake(){clear(ndc=lst=1);}
	void insert(char c, int id){
		int cur=clear(++ndc), p=lst, cc=c-'a';siz[cur]=1;
		sam[cur].len=sam[lst].len+1;
		for(; p&&!sam[p].nxt[cc]; p=sam[p].fa)
			sam[p].nxt[cc]=cur;
		int q=sam[p].nxt[cc];
		if(!q) sam[cur].fa=1;
		else if(sam[q].len==sam[p].len+1) sam[cur].fa=q;
		else{
			int nex=++ndc;sam[nex]=sam[q], sam[nex].len=sam[p].len+1;
			for(; p&&sam[p].nxt[cc]==q; p=sam[p].fa) 
				sam[p].nxt[cc]=nex;
			sam[cur].fa=sam[q].fa=nex;
		}
		pos[id]=lst=cur;
	}
	bool check(int m, char *t){
		int u=1;
		for(int i=1; i<=m&&u; ++i) u=sam[u].nxt[t[i]-'a'];
		return u>0;
	}
	#define ls(u) lc[u]
	#define rs(u) rc[u]
	#define mid (l+r>>1)
	int tot, rt[Nc], lc[Nc*30], rc[Nc*30];
	void update(int &k, int l, int r, int x){
		if(!k) k=++tot;if(l==r) return ;
		if(x<=mid) update(ls(k), l, mid, x);
		else update(rs(k), mid+1, r, x);
	}
	int merge(int p, int q){
		if(!p||!q) return p+q;int o=++tot;
		ls(o)=merge(ls(p), ls(q)), rs(o)=merge(rs(p), rs(q));
		return o;
	}
	int query(int k, int l, int r, int x){//find the left in [x, n]
		if(!k||x>r) return 0;
		if(l==r) return mid;
		int o=query(ls(k), l, mid, x);
		return o?o:query(rs(k), mid+1, r, x);
	}
	#undef ls
	#undef rs
	#undef mid
	vi G[Nc];
	void dfs(int x){
		for(int i=1; i<20; ++i) f[x][i]=f[f[x][i-1]][i-1];
		for(auto v:G[x]) f[v][0]=x, dfs(v), siz[x]+=siz[v], rt[x]=merge(rt[x], rt[v]);
	}
	int find(int x, int k){for(int i=19; i>=0; --i) if(sam[f[x][i]].len>=k) x=f[x][i];return x;}
	void build(int n){
		for(int i=2; i<=ndc; ++i) G[sam[i].fa].pb(i);
		for(int i=1; i<=n; ++i) update(rt[pos[i]], 1, n, i);
		dfs(1);
	}
} 

本质不同子串个数

本质不同子串个数就是 SAM 上每个结点的 endpos 等价类的大小,即 \(len_{x}-len_{fa_{x}}\)。

最长公共后缀 \(\operatorname{LSP}\)

求两个子串的 \(\operatorname{LSP}\),定位结点后,它们的 \(\operatorname{LCA}\) 及往上都是它们的 \(SP\),最长的就是 \(Mx_{\operatorname{LCA}}\),长度就是 \(len_{\operatorname{LCA}}\)。

\(\operatorname{LCP}\) 的话,把串反过来就变成 \(\operatorname{LSP}\) 了。

posted @ 2022-11-06 15:16  127_127_127  阅读(114)  评论(0)    收藏  举报