后缀自动机专题练习

SAM

一、SAM的性质:

S A M SAM SAM是个状态机。一个起点,若干终点。原串的所有子串和从 S A M SAM SAM起点开始的所有路径一一对应,不重不漏。所以终点就是包含后缀的点。
每个点包含若干子串,每个子串都一一对应一条从起点到该点的路径。且这些子串一定是里面最长子串的连续后缀。
S A M SAM SAM问题中经常考虑两种边:
( 1 ) (1) (1) 普通边,类似于 T r i e Trie Trie。表示在某个状态所表示的所有子串的后面添加一个字符。
( 2 ) (2) (2) L i n k 、 F a t h e r Link、Father LinkFather。表示将某个状态所表示的最短子串的首字母删除。这类边构成一棵树。

二、SAM的构造思路

e n d p o s ( s ) endpos(s) endpos(s):子串s所有出现的位置(尾字母下标)集合。 S A M SAM SAM中的每个状态都一一对应一个 e n d p o s endpos endpos的等价类。

e n d p o s endpos endpos的性质:
( 1 ) (1) (1) s 1 , s 2 s_1,s_2 s1,s2 S S S 的两个子串 ,不妨设 ∣ s 1 ∣ ≤ ∣ s 2 ∣ |s_1|≤|s_2| s1s2 (我们用 ∣ s ∣ |s| s 表示 s s s 的长度 ,此处等价于 s 1 s_1 s1 不长于 s 2 s_2 s2 )。则 s 1 s_1 s1 s 2 s_2 s2 的后缀当且仅当 e n d p o s ( s 1 ) ⊇ e n d p o s ( s 2 ) endpos(s1)⊇endpos(s2) endpos(s1)endpos(s2) s 1 s_1 s1 不是 s 2 s_2 s2 的后缀当且仅当 e n d p o s ( s 1 ) ∩ e n d p o s ( s 2 ) = ∅ endpos(s1)∩endpos(s2)=∅ endpos(s1)endpos(s2)= 。
( 2 ) (2) (2)两个不同子串的 e n d p o s endpos endpos,要么有包含关系,要么没有交集。
( 3 ) (3) (3) 两个子串的 e n d p o s endpos endpos相同,那么短串为长串的后缀。
( 4 ) (4) (4) 对于一个状态 s t st st ,以及任意的 l o n g e s t ( s t ) longest(st) longest(st) 的后缀 s s s ,如果 s s s 的长度满足: ∣ s h o r t e s t ( s t ) ∣ ≤ ∣ s ∣ ≤ ∣ l o n g s e s t ( s t ) ∣ |shortest(st)|≤|s|≤|longsest(st)| shortest(st)slongsest(st) ,那么 s ∈ s u b s t r i n g s ( s t ) s∈substrings(st) ssubstrings(st)

三、SAM的构造过程
分类讨论,具体看板书。
证明较为复杂,略。

四、SAM时间复杂度
线性。
证明较为复杂,略。

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/585844/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


AcWing 2766. 后缀自动机

在这里插入图片描述

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 2e6 + 10;

char str[N];
struct Node
{
	int len, fa; //len该点表示的集合内最长串的长度,fa最短串去掉首字母后的子串所在状态
	int ch[26];
} node[N];
int tot = 1, last = 1;
LL cnt[N], ans; //cnt[i] 状态i表示的endpos(i)的元素个数

void  extend(int c)
{
	int p = last, np = last = ++ tot;
	cnt[tot] = 1;//包含原串前缀的状态的cnt=1
	node[np].len = node[p].len + 1;

	for( ; p && !node[p].ch[c]; p = node[p].fa)	node[p].ch[c] = np;
	if(!p)	node[np].fa = 1;
	else
	{
		int q = node[p].ch[c];
		if(node[q].len == node[p].len + 1)	node[np].fa = q;
		else
		{
			int nq = ++ tot;
			node[nq] = node[q], node[nq].len = node[p].len + 1;
			node[q].fa = node[np].fa = nq;
			for( ; p && node[p].ch[c] == q; p = node[p].fa)	node[p].ch[c] = nq;
		}
	}
}

int idx, head[N], e[N], ne[N];
void add(int u, int v)
{
	e[++ idx] = v, ne[idx] = head[u], head[u] = idx;
}

void dfs(int u)
{
	for(int i = head[u]; i; i = ne[i])
	{
		dfs(e[i]);
		cnt[u] += cnt[e[i]];
	}
	if(cnt[u] > 1)	ans = max(ans, cnt[u] * node[u].len);
}

int main()
{
#ifdef LOCAL
	freopen("in.in", "r", stdin);
	freopen("out.out", "w", stdout);
#endif

	cin >> str;
	//逐个插入,构建str的后缀自动机
	for(int i = 0; str[i]; i ++)	extend(str[i] - 'a');
	
	for(int i = 2; i <= tot; i ++) //构建parent树,1点没有父节点
		add(node[i].fa, i);
	
	dfs(1); //递归求cnt
	cout << ans << endl;

	return 0;
}

AcWing 1283. 玄武密码

在这里插入图片描述

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 1e7 + 10;

int n, m;
char str[N];
struct Node
{
	int len, fa;
	int ch[4];
} node[N << 1];
int tot = 1, last = 1;

int get(char ch)
{
	if(ch == 'E')	return 0;
	if(ch == 'S')	return 1;
	if(ch == 'W')	return 2;
	return 3;
}

void extend(int c)
{
	int p = last, np = last = ++ tot;
	node[np].len = node[p].len + 1;

	for(; p && !node[p].ch[c]; p = node[p].fa)	node[p].ch[c] = np;
	if(!p)	node[np].fa = 1;
	else
	{
		int q = node[p].ch[c];
		if(node[q].len == node[p].len + 1)	node[np].fa = q;
		else
		{
			int nq = ++ tot;
			node[nq] = node[q], node[nq].len = node[p].len + 1;
			node[q].fa = node[np].fa = nq;
			for(; p && node[p].ch[c] == q; p = node[p].fa)	node[p].ch[c] = nq;
		}
	}
}

int main()
{
#ifdef LOCAL
	freopen("in.in", "r", stdin);
	freopen("out.out", "w", stdout);
#endif

	scanf("%d%d", &n, &m);
	scanf("%s", str);
	for(int i = 0; i < n; i ++)	extend(get(str[i]));

	while(m --)
	{
		scanf("%s", str);
		int ans = 0, p = 1;
		for(int i = 0; str[i]; i ++) //从串的前缀沿着边走就行了,类似Tire
		{
			int c = get(str[i]);
			if(node[p].ch[c])	p = node[p].ch[c], ans ++;
			else	break;
		}
	    printf("%d\n", ans);
	}

	return 0;
}

AcWing 2811. 最长公共子串

在这里插入图片描述

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 1e4 + 10, M = 2e4 + 10;

int n;
char str[N];
struct Node
{
	int len, fa;
	int ch[26];
} node[M];
int tot = 1, last = 1;
int now[M], ans[M];
//now[i]当前串在i这个状态集合里面匹配的最大长度

void extend(int c) //模版背过
{
	int p = last, np = last = ++ tot;
	node[np].len = node[p].len + 1;
	for(; p && !node[p].ch[c]; p = node[p].fa)	node[p].ch[c] = np;
	if(!p)	node[np].fa = 1;
	else
	{
		int q = node[p].ch[c];
		if(node[q].len == node[p].len + 1)	node[np].fa = q;
		else
		{
			int nq = ++ tot;
			node[nq] = node[q], node[nq].len = node[p].len + 1;
			node[q].fa = node[np].fa = nq;
			for(; p && node[p].ch[c] == q; p = node[p].fa)	node[p].ch[c] = nq;
		}
	}
}

int idx, head[M], e[M], ne[M];
void add(int u, int v)
{
	e[++ idx] = v, ne[idx] = head[u], head[u] = idx;
}

void dfs(int u)
{
	for(int i = head[u]; i; i = ne[i])
	{
		dfs(e[i]);
		now[u] = max(now[u], min(now[e[i]], node[u].len));
	}
}

int main()
{
#ifdef LOCAL
	freopen("in.in", "r", stdin);
	freopen("out.out", "w", stdout);
#endif

	scanf("%d%s", &n, str);
	for(int i = 0; str[i]; i ++)	extend(str[i] - 'a'); //构建第一个串的后缀自动机
	
	for(int i = 2; i <= tot; i ++)	add(node[i].fa, i);
	memset(ans, 0x3f, sizeof ans);
	
	for(int i = 2; i <= n; i ++) //用每一个串与第一个串匹配
	{
		scanf("%s", str);
		memset(now, 0, sizeof now);

		int t = 0, p = 1; //当前匹配长度, 当前在后缀自动机中的状态
		for(int j = 0; str[j]; j ++)
		{
			int c = str[j] - 'a';
			while(p > 1 && !node[p].ch[c])	p = node[p].fa, t = node[p].len;
			//如果当前状态没有c的出边,则沿着fa边向上走
			//直到p存在c的出边或走到了空
			if(node[p].ch[c])	p = node[p].ch[c], t ++; //如果是p存在c边的情况,则匹配长度+1
			now[p] = max(now[p], t);
		}
		dfs(1); //向上传递一下now,更新fa的now
		for(int j = 1; j <= tot; j ++)	ans[j] = min(ans[j], now[j]); //对于每一个串匹配的now,取一个min
	}

	int res = 0;
	for(int i = 1; i <= tot; i ++)	res = max(res, ans[i]); //在所有的状态中取一个max
	printf("%d\n", res);

	return 0;
}

P4070 [SDOI2016]生成魔咒

在这里插入图片描述

每插入一个魔咒字符 x x x(注意,在这里面 12 12 12相当于是一个字符,被坑惨了),求当前字符串 S S S本质不同的子串数量。
在这里,每一个状态可能有的出边有 1 e 9 1e9 1e9种,肯定不能开一个 2 ∗ 1 e 5 ∗ 1 e 9 2*1e5*1e9 21e51e9的数组。但是能用到的出边却不会很多,所以结构体里面的 c h ch ch数组可以开成一个 m a p < i n t , i n t > c h map<int,int> ch map<int,int>ch

还有,每加入一个字符,新增的子串个数就是新建出来的节点 n p np np的贡献, n q nq nq对答案是没有贡献的。

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 2e5 + 10;

struct Node
{
	int len, fa;
	map<int, int> ch;
} node[N];
int tot = 1, last = 1;

void extend(int c)
{
	int p = last, np = last = ++ tot;
	node[np].len = node[p].len + 1;
	for(; p && !node[p].ch[c]; p = node[p].fa)	node[p].ch[c] = np;
	if(!p)	node[np].fa = 1;
	else
	{
		int q = node[p].ch[c];
		if(node[q].len == node[p].len + 1)	node[np].fa = q;
		else
		{
			int nq = ++ tot;
			node[nq] = node[q], node[nq].len = node[p].len + 1;
			node[q].fa = node[np].fa = nq;
			for(; p && node[p].ch[c] == q; p = node[p].fa)	node[p].ch[c] = nq;
		}
	}
}

int main()
{
#ifdef LOCAL
	freopen("in.in", "r", stdin);
	freopen("out.out", "w", stdout);
#endif

	int n; LL ans = 0;
	char str[15];
	scanf("%d", &n);
	
	while(n --)
	{
		int x; scanf("%d", &x);
		extend(x);
		ans += node[last].len - node[node[last].fa].len;
		printf("%lld\n", ans);
	}

	return 0;
}

字典序第k小子串

在这里插入图片描述

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 1e5 + 10, M = N * 2;

int n, m;
char str[N];
struct Node
{
	int len, fa;
	int ch[26];
} node[M];
int tot = 1, last = 1;

void extend(int c)
{
	int p = last, np = last = ++ tot;
	node[np].len = node[p].len + 1;
	for(; p && !node[p].ch[c]; p = node[p].fa)	node[p].ch[c] = np;
	if(!p)	node[np].fa = 1;
	else
	{
		int q = node[p].ch[c];
		if(node[q].len == node[p].len + 1)	node[np].fa = q;
		else
		{
			int nq = ++ tot;
			node[nq] = node[q], node[nq].len = node[p].len + 1;
			node[q].fa = node[np].fa = nq;
			for(; p && node[p].ch[c] == q; p = node[p].fa)	node[p].ch[c] = nq;
		}
	}
}

int to[M]; //to[i]:从状态i的路径条数
int dfs(int u)
{
	if(to[u])	return to[u]; //防止重复计算
	to[u] = 1;
	for(int i = 0; i < 26; i ++)
	{
		int v = node[u].ch[i];
		if(!v)	continue;
		to[u] += dfs(v);
	}
	return to[u];
}

void find(int k) //找第k小子串
{
	int p = 1; //从root开始
	while(k)
	{
		if(!k)	break;
		for(int i = 0; i < 26; i ++) //从小到大,如果是求第k大,可以从大到小
		{
			int q = node[p].ch[i];
			if(!q)	continue;

			if(k > to[q])	k -= to[q];
			else
			{
				putchar(i + 'a'); //边找边输出
				p = q; k --; //这里k一定要-1
				break;
			}
		}
	}
	puts("");
}

int main()
{
#ifdef LOCAL
	freopen("in.in", "r", stdin);
	freopen("out.out", "w", stdout);
#endif

	scanf("%s%d", str, &m);
	for(int i = 0; str[i]; i ++)	extend(str[i] - 'a');

	dfs(1);

	while(m --)
	{
		int k; scanf("%d", &k);
		find(k);
	}

	return 0;
}

P3975 [TJOI2015]弦论

在这里插入图片描述

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 5e5 + 10, M = 1e6 + 10;

int n, m, t, k;
char str[N];
struct Node
{
	int len, fa;
	int ch[26];
} node[M];
int tot = 1, last = 1;
int sz[M]; //sz[i]:endpos(i)集合的大小,(出现次数)
LL sum[M]; //sum[i]:点i能够到达的本质不同的字符串个数

void extend(int c)
{
	int p = last, np = last = ++ tot;
	sz[np] = 1;
	node[np].len = node[p].len + 1;
	for(; p && !node[p].ch[c]; p = node[p].fa)	node[p].ch[c] = np;
	if(!p)	node[np].fa = 1;
	else
	{
		int q = node[p].ch[c];
		if(node[q].len == node[p].len + 1)	node[np].fa = q;
		else
		{
			int nq = ++ tot;
			node[nq] = node[q], node[nq].len = node[p].len + 1;
			node[q].fa = node[np].fa = nq;
			for(; p && node[p].ch[c] == q; p = node[p].fa)	node[p].ch[c] = nq;
		}
	}
}

int idx, head[M], e[M], ne[M];
void add(int u, int v)
{
	e[++ idx] = v, ne[idx] = head[u], head[u] = idx;
}

void get_size(int u) //求sz
{
	for(int i = head[u]; i; i = ne[i])
	{
		int v = e[i];
		get_size(v);
		sz[u] += sz[v];
	}
}

bool vis[M];
void dfs(int u) //求sum
{
	if(vis[u])	return ;
	vis[u] = true;
	for(int i = 0; i < 26; i ++)
	{
		int v = node[u].ch[i];
		dfs(v);
		sum[u] += sum[v];
	}
}

void pr(int k)
{
	int p = 1;
	while(k)
	{
		for(int i = 0; i < 26; i ++)
		{
			int q = node[p].ch[i];
			if(!q)	continue;

			if(k > sum[q])	k -= sum[q];
			else
			{
				putchar(i + 'a');
				p = q, k -= sz[p]; //这里减去的是sz[p]
				break;
			}
		}
	}
	putchar('\n');
}

int main()
{
#ifdef LOCAL
	freopen("in.in", "r", stdin);
	freopen("out.out", "w", stdout);
#endif

	scanf("%s", str);
	for(int i = 0; str[i]; i ++)	extend(str[i] - 'a');

	scanf("%d%d", &t, &k);

	for(int i = 2; i <= tot; i ++)	add(node[i].fa, i);
	get_size(1);

	for(int i = 1; i <= tot; i ++)	
		t ? (sum[i] = sz[i]) : (sum[i] = sz[i] = 1);
	sz[1] = sum[1] = 0; //去掉sz[1]代表的空串

	dfs(1);

	if(sum[1] < k)	puts("-1");
	else	pr(k);

	return 0;
}

exSAM

S A M SAM SAM是构建一个字符串的后缀自动机, e x S A M exSAM exSAM是将多个串构建在一个后缀自动机中。

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

在这里插入图片描述

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <set>
#include <map>
#include <queue>
#include <stack>
#include <vector>
#include <string>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

typedef long long LL;

const int N = 2e6 + 10, MOD = 9982443553;

int n, m;
char str[N];
struct Node
{
	int len, fa;
	int ch[26];
} node[N];
int tot = 1, last = 1;

void extend(int c)
{
	if(node[last].ch[c])//此处新增代码就是在线版的广义后缀自动机的插入
	{
		int p = last, q = node[p].ch[c];
		if(node[q].len == node[p].len + 1)	last = q;
		else
		{
			int nq = ++ tot;
			node[nq] = node[q], node[nq].len = node[p].len + 1;
			for(; p && node[p].ch[c] == q; p = node[p].fa)	node[p].ch[c] = nq;
			node[q].fa = last = nq;
		}
		return ;
	}
	//以下代码与后缀自动机的插入完全一致
	int p = last, np = last = ++ tot;
	node[np].len = node[p].len + 1;
	for(; p && !node[p].ch[c]; p = node[p].fa)	node[p].ch[c] = np;
	if(!p)	node[np].fa = 1;
	else
	{
		int q = node[p].ch[c];
		if(node[q].len == node[p].len + 1)	node[np].fa = q;
		else
		{
			int nq = ++ tot;
			node[nq] = node[q], node[nq].len = node[p].len + 1;
			node[q].fa = node[np].fa = nq;
			for(; p && node[p].ch[c] == q; p = node[p].fa)	node[p].ch[c] = nq;
		}
	}
}

int main()
{
#ifdef LOCAL
	freopen("in.in", "r", stdin);
	freopen("out.out", "w", stdout);
#endif

	scanf("%d", &n);
	for(int i = 0; i < n; i ++)
	{
		scanf("%s", str); last = 1; //每次新插入一个串就将last置成起点
		for(int j = 0; str[j]; j ++)	extend(str[j] - 'a');
	}

    LL ans = 0;
    for(int i = 2; i <= tot; i ++)
        ans += node[i].len - node[node[i].fa].len;
    printf("%lld\n", ans);

	return 0;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值