SAM胡诌

写在前面

肝硬化太菜了发现根本不会后缀数组(SA)和后缀自动机(SAM),曾经的我非常懦弱而畏惧学习 SAM 而企图学习 SA 萌混过关,但是发现患有痴呆症的 $rAIn$ 对于 SA 的学习根本就是学一次忘一次,于是牠鼓起勇气打起精神学习了 SAM………

推歌:《炉心熔解》。


SAMの定义内容性质特点与其他东西的异同意义影响炼字修辞环境描写插叙倒叙比兴重章叠句一词多义古今异义

由于各种大手子都在争相认为自己是区,肝硬化显然比神犇蒻 inf 倍不止,牠需要一个比区还要微小的形容词来形容自己,于是牠钦定牠是石。

石当然讲不明白 SAM 这么深刻的事物,牠收集了一篇《深度好文》。

这个奶妈写得相当清晰易懂!!!

这里给出一个肝硬化制造的清晰好贺(bushi)好背的板子!
#define trie(x) sam[(x)]. trie
#define lnk(x) sam[(x)]. lnk
#define len(x) sam[(x)]. len
struct hhh{
	int trie[27], lnk, len;
}sam[_ << 1];
inline void insert(int x){
	int p = bfr, np = bfr = ++ cnt;
	len(np) = len(p) + 1;
	for(; p && ! trie(p)[x]; p = lnk(p)){
		trie(p)[x] = np;
	}
	if(! p){
		lnk(np) = 1;
	}
	else{
		int q = trie(p)[x];
		if(len(q) == len(p) + 1){
			lnk(np) = q;
		}
		else{
			int nw = ++ cnt;
			sam[nw] = sam[q];
			len(nw) = len(p) + 1;
			lnk(q) = lnk(np) = nw;
			for(; p && trie(p)[x] == q; p = lnk(p)){
				trie(p)[x] = nw;
			}
		}
	}
	return ;
}

然后大家就可以爆切 SAM 啦!(欸我到底在和谁对话………)


【模板】后缀自动机(SAM)

想自己总结来着但是发现用自己的语言好像说不很通顺于是图省事直接粘了上面推荐的博文的一段话………
image

因为在同一个等价类中,每个串出现次数相同,显然长度最长的串才可能对答案产生贡献,于是我们做树形 DP 统计出现次数再直接乘上 len 即可。

Code
#include <bits/stdc++.h>
using namespace std;
const int _ = 1000010;
int n, to[_ << 1], nxt[_ << 1], h[_ << 1], tot, ci[_ << 1], bfr = 1, cnt = 1;
long long ans;
string s;
inline void add(int x, int y){
	to[++ tot] = y;
	nxt[tot] = h[x];
	h[x] = tot;
	return ;
}
#define trie(x) sam[(x)]. trie
#define lnk(x) sam[(x)]. lnk
#define len(x) sam[(x)]. len
struct hhh{
	int trie[27], lnk, len;
}sam[_ << 1];
inline void insert(int x){
	int p = bfr, np = bfr = ++ cnt;
	ci[np] = 1, len(np) = len(p) + 1;
	for(; p && ! trie(p)[x]; p = lnk(p)){
		trie(p)[x] = np;
	}
	if(! p){
		lnk(np) = 1;
	}
	else{
		int q = trie(p)[x];
		if(len(q) == len(p) + 1){
			lnk(np) = q;
		}
		else{
			int nw = ++ cnt;
			sam[nw] = sam[q];
			len(nw) = len(p) + 1;
			lnk(q) = lnk(np) = nw;
			for(; p && trie(p)[x] == q; p = lnk(p)){
				trie(p)[x] = nw;
			}
		}
	}
	return ;
}
inline void dfs(int x){
	for(int i = h[x]; i; i = nxt[i]){
		int y = to[i];
		dfs(y);
		ci[x] += ci[y];
	}
	if(ci[x] > 1){
		ans = max(ans, 1ll * ci[x] * len(x));
	}
	return ;
}
int main(){
	ios :: sync_with_stdio(0), cin. tie(0), cout. tie(0);
	cin >> s;
	n = s. size();
	for(int i = 0; i < n; i ++){
		insert(s[i] - 'a');
	}
	for(int i = 2; i <= cnt; i ++){
		add(lnk(i), i);
	}
	dfs(1);
	cout << ans;
	return 0;
}

[SDOI2016] 生成魔咒

题意:给定一个字符串,询问它的每个前缀有多少种本质不同的子串。


Solve

由于 SAM 每次新插入一个字符都不会使曾经的子串消失,于是我们考虑 endpos 集合,一个从未出现过的子串一定是一个新的等价类,并且这个等价类里面只有“当前插入的字符处于原串的位置”这一个位置,否则的话肯定这个等价类里的串曾经已经出现过了,一定不会有新的贡献。于是这个等价类里所有串的数量就是加入这个字符所产生的贡献。由于一个等价类里串的长度是连续的,于是我们用 \(len(now) - len(link(now))\) 即可算出新的贡献,加入答案输出即可。

Code
#include <bits/stdc++.h>
using namespace std;
const int _ = 100010;
int n, x, cnt = 1, bfr = 1, lnk[_ << 1], len[_ << 1];
map<int, int> trie[_ << 1];
long long ans;
inline void insert(int x){
	int p = bfr, np = bfr = ++ cnt;
	len[np] = len[p] + 1;
	for(; p && ! trie[p][x]; p = lnk[p]){
		trie[p][x] = np;
	}
	if(! p){
		lnk[np] = 1;
	}
	else{
		int q = trie[p][x];
		if(len[q] == len[p] + 1){
			lnk[np] = q;
		}
		else{
			int nw = ++ cnt;
			trie[nw] = trie[q];
			lnk[nw] = lnk[q];
			len[nw] = len[p] + 1;
			lnk[np] = lnk[q] = nw;
			for(; p && trie[p][x] == q; p = lnk[p]){
				trie[p][x] = nw;
			}
		}
	}
	ans += len[np] - len[lnk[np]];
	return ;
}
int main(){
	ios :: sync_with_stdio(0), cin. tie(0), cout. tie(0);
	cin >> n;
	for(int i = 1; i <= n; i ++){
		cin >> x;
		insert(x);
		cout << ans << '\n';
	}
	return 0;
}

[TJOI2015] 弦论

题意:给定一个字符串,问字典序第 k 小的子串是什么。它使用了一些特殊手段让你需要把“不同位置的相同子串算作一个”与“不同位置的相同子串算作多个”的情况都考虑。


Solve

考虑“不同位置的相同子串算作多个”怎么做,发现 endpos 集合的大小是建出后缀树之后每个节点的子树和。

考虑“不同位置的相同子串算作一个”怎么做,发现根本就是不用子树和,直接记它的 endpos 集合大小为 1。

然后如果会使用线段树查找 k 小值的话那么在 SAM 上查 k 小值就是平凡的。


【AHOI 2013】差异

题意:给定一个长度为 \(n\) 的字符串 \(S\),令 \(T_i\) 表示它从第 \(i\) 个字符开始的后缀。求

\[\sum_{1\leqslant i<j\leqslant n}len(T_i)+len(T_j)-2*lcp(T_i,T_j) \]

其中,\(\text{len}(a)\) 表示字符串 \(a\) 的长度,\(\text{lcp}(a,b)\) 表示字符串 \(a\) 和字符串 \(b\) 的最长公共前缀。


Solve

前两项是个常数我们可以推出这样的式子: $$\sum_{1<=i<n}i(n-i)+(n+i+1)(n-i)/2$$

然后拆开再用连续自然数平方和公式:\(1² + 2² + … + n² = n(n+1)(2n+1)/6\) 算出其等于 \((n^3-n)/2\)

之后因为 SAM 可以通过跳后缀树上两个节点的 LCA 处理前缀的最长公共后缀,那么建反串的 SAM 就可以找原串的后缀的最长公共前缀。

那么我们考虑树形 DP,把每个点作为 LCA 考虑,求出这个结点对答案的贡献,然后对每个结点的贡献求和,就是所需的答案了。

但是这样对于两个节点 i、j,我们会计算 (i,j) 和 (j,i) 的答案,重了。但是由于题面要求的式子中有个乘二,于是不用将答案除以二。

Code
#include <bits/stdc++.h>
using namespace std;
const int _ = 500010;
int n, cnt = 1, bfr = 1, to[_ << 1], nxt[_ << 1], tot, h[_ << 1];
long long ans, siz[_ << 1], g[_ << 1];
string s;
inline void add(int x, int y){
	to[++ tot] = y;
	nxt[tot] = h[x];
	h[x] = tot;
	return ;
}
#define trie(x) sam[(x)]. trie
#define len(x) sam[(x)]. len
#define lnk(x) sam[(x)]. lnk
struct hhh{
	int trie[27], lnk, len;
}sam[_ << 1];
inline void insert(int x){
	int p = bfr, np = bfr = ++ cnt;
	len(np) = len(p) + 1;
	g[np] = 1;
	for(; p && ! trie(p)[x]; p = lnk(p)){
		trie(p)[x] = np;
	}
	if(! p){
		lnk(np) = 1;
	}
	else{
		int q = trie(p)[x];
		if(len(q) == len(p) + 1){
			lnk(np) = q;
		}
		else{
			int nw = ++ cnt;
			sam[nw] = sam[q];
			len(nw) = len(p) + 1;
			lnk(np) = lnk(q) = nw;
			for(; p && trie(p)[x] == q; p = lnk(p)){
				trie(p)[x] = nw;
			}
		}
	}
	return ;
}
inline void dfs(int x){
	siz[x] = g[x];
	for(int i = h[x]; i; i = nxt[i]){
		int y = to[i];
		dfs(y);
		siz[x] += siz[y];
	}
	ans -= (siz[x] - g[x]) * g[x] * len(x);
	for(int i = h[x]; i; i = nxt[i]){
		int y = to[i];
		ans -= siz[y] * (siz[x] - siz[y]) * len(x);
	}
}
int main(){
	ios :: sync_with_stdio(0), cin. tie(0), cout. tie(0);
	cin >> s;
	n = s. size();
	for(int i = n - 1; i >= 0; i --){
		insert(s[i] - 'a');
	}
	for(int i = 1; i < n; i ++){
		ans += 1ll * i * (n - i) + (1ll * (n + i + 1) * (n - i) >> 1);
	}
	for(int i = 2; i <= cnt; i ++){
		add(lnk(i), i);
	}
	dfs(1);
	cout << ans;
	return 0;
}

[TJOI2019] 甲苯先生和大中锋的字符串

题意:求在字符串中恰好出现了 k 次的所有子串中其长度出现次数最多的长度数(如果有多个输出最长长度)

先别急着往下看,先尝试断一下句qwq………

实在困难就先打开原题玩一下样例………


Solve

首先做出了前面的题们现在对于大家来说找出出现 k 次的等价类是平凡的(就是建出 后缀树之后求子树和再判断是否等于 k)。

由于一个等价类中所有字符串的长度是连续不重复的,那么其中的字符串的长度就分别为 [len(link(x))+1,len(x) ] 这个区间的所有自然数。

于是就变成了区间修改全局查最大值的状物,无脑拍一棵线段树就没了。

Code
#include <bits/stdc++.h>
using namespace std;
const int _ = 100010;
int k, n, t, cnt, bfr, to[_ << 1], nxt[_ << 1], tot, h[_ << 1], g[_ << 1];
string s;
#define ls (root << 1)
#define rs (root << 1 | 1)
#define mid ((l + r) >> 1)
#define mx(x) tree[(x)]. mx
#define l(x) tree[(x)]. l
struct rain{
	int mx, l;
}tree[_ << 2];
inline void build(int root, int l, int r){
	tree[root] = {0, 0};
	if(l >= r){
		return ;
	}
	build(ls, l, mid);
	build(rs, mid + 1, r);
	return ;
}
inline void pushdown(int root){
	if(l(root)){
		l(ls) += l(root), mx(ls) += l(root), l(rs) += l(root), mx(rs) += l(root);
		l(root) = 0;
	}
	return ;
}
inline void pushup(int root){
	mx(root) = max(mx(ls), mx(rs));
	return ;
}
inline void update(int root, int l, int r, int x, int y, int v){
	if(l >= x && r <= y){
		mx(root) += v, l(root) += v;
		return ;
	}
	pushdown(root);
	if(x <= mid){
		update(ls, l, mid, x, y, v);
	}
	if(y > mid){
		update(rs, mid + 1, r, x, y, v);
	}
	pushup(root);
	return ;
}
#define trie(x) sam[(x)]. trie
#define lnk(x) sam[(x)]. lnk
#define len(x) sam[(x)]. len
struct hhh{
	int trie[27], lnk, len;
}sam[_ << 1];
inline void add(int x, int y){
	to[++ tot] = y;
	nxt[tot] = h[x];
	h[x] = tot;
	return ;
}
inline void insert(int x){
	int p = bfr, np = bfr = ++ cnt;
	len(np) = len(p) + 1;
	g[np] = 1;
	for(; p && ! trie(p)[x]; p = lnk(p)){
		trie(p)[x] = np;
	}
	if(! p){
		lnk(np) = 1;
	}
	else{
		int q = trie(p)[x];
		if(len(q) == len(p) + 1){
			lnk(np) = q;
		}
		else{
			int nw = ++ cnt;
			g[nw] = 0;
			sam[nw] = sam[q];
			len(nw) = len(p) + 1;
			lnk(np) = lnk(q) = nw;
			for(; p && trie(p)[x] == q; p = lnk(p)){
				trie(p)[x] = nw;
			}
		}
	}
	return ;
}
inline void dfs(int x){
	for(int i = h[x]; i; i = nxt[i]){
		int y = to[i];
		dfs(y);
		g[x] += g[y];
	}
	return ;
}
inline int query(int root, int l, int r){
	if(l == r){
		return l;
	}
	pushdown(root);
	if(mx(rs) >= mx(ls)){
		return query(rs, mid + 1, r);
	}
	return query(ls, l, mid);
}
int main(){
	ios :: sync_with_stdio(0), cin. tie(0), cout. tie(0);
cin >> t;
while(t --){
	build(1, 1, n);
	memset(sam, 0, sizeof(sam));
	memset(h, tot = g[1] = 0, sizeof(h));
	cin >> s >> k;
	n = s. size();
	cnt = bfr = 1;
	for(int i = 0; i < n; i ++){
		insert(s[i] - 'a');
	}
	for(int i = 2; i <= cnt; i ++){
		add(lnk(i), i);
//		cout << lnk(i) << ' ' <<i << '\n';
	}
//	for(int i = 1; i <= cnt; i ++){
//		cout << g[i] << ' ';
//	}cout << '\n';
	dfs(1);
//	for(int i = 1; i <= cnt; i ++){
//		cout << g[i] << ' ';
//	}cout << '\n';
	for(int i = 2; i <= cnt; i ++){
		if(g[i] == k){
//			cout << len(lnk(i)) << ' ' << len(i) << ' ' << i <<'\n';
			update(1, 1, n, len(lnk(i)) + 1, len(i), 1);
		}
	}
	if(! mx(1)){
		cout << - 1 << '\n';
		continue;
	}
	cout << query(1, 1, n) << '\n';
}
	return 0;
}

The End.(有点草率但是 Rain 真的只会写板子喵………)

posted @ 2026-08-15 22:05  养鸡大户肝硬化  阅读(9)  评论(0)    收藏  举报