再谈后缀自动机


为什么叫再谈后缀自动机呢?
因为以前学过一次,但是没有太学懂。
愈发觉得学懂一个算法比草率学很多算法更有用了。


1.学习链接

Menci的SAM
某大佬的SAM
自己以前的博客
Oiwiki

2.个人理解

在SAM中,需要了解的事情:
SAM中有一个有向无环单词图以及一个后缀树,每个节点都同时存在于这两个结构中。

节点 u u u:从起点开始到 u u u走过的路径组成的字符串(母串的子串)的集合。每个子串都是以 u u u结尾的前缀的一组连续长度后缀,即 m i n l e n ( u ) minlen(u) minlen(u) m a x l e n [ u ] maxlen[u] maxlen[u]这段长度的子串。并且,这些子串的 e n d p o s endpos endpos集合相同。

e n d p o s ( u ) endpos(u) endpos(u) u u u节点代表子串出现的末尾位置的集合

t r a n s [ u ] [ c ] trans[u][c] trans[u][c]:表示的是 u u u代表的子串后面接 c c c转移到的节点

m a x l e n [ u ] maxlen[u] maxlen[u]:代表 u u u节点的长度最大子串的长度

l i n k [ u ] link[u] link[u]:后缀链接代表了 l i n k [ u ] link[u] link[u] u u u的后缀,但是 e n d p o s ( l i n k [ u ] ) endpos(link[u]) endpos(link[u])会有所增加,且满足条件: m a x l e n [ l i n k [ u ] ] + 1 = m i n l e n ( u ) maxlen[link[u]] + 1 = minlen(u) maxlen[link[u]]+1=minlen(u),如果从 u u u一直跳 l i n k link link到初始节点,就表示了 u u u最长串的所有后缀。

有向无环单词图: t r a n s trans trans
后缀树: l i n k link link进行反向后,构成的树

实际上在SAM中存放的数据: m a x l e n [ ] , t r a n s [ ] [ ] , l i n k [ ] maxlen[], trans[][], link[] maxlen[],trans[][],link[]

e n d p o s ( ) endpos() endpos()集合大小的表示:
考虑增量法,最开始 e n d p o s ( ) endpos() endpos()的大小为1,每次增加一个字符,需要新增一个节点表示最长的那个后缀(自己本身),然后对于其他后缀,采取 e n d p o s endpos endpos相同的就合并过去的操作,建立 l i n k link link连接,那么对于每个点而言(除了建SAM的第三种情况新拆的点),有多少个点跳 l i n k link link能到 u u u,那么它的 e n d p o s endpos endpos的大小就是多少。

3.具体操作及实现细节

hihocoder1449

一句话题意:求长度为k的出现最多的子串的出现次数

其实就是求 e n d p o s endpos endpos的大小

这里的 v a l val val最初赋值为1,然后进行拓扑排序。
但是考虑每个点,会对从 u u u到起始节点的路径上的所有点都有1的贡献,而那个贡献长度的区间是 m i n min min m a x max max,这样还是 O ( n 2 ) O(n^2) O(n2)的复杂度。考虑用 m a x l e n maxlen maxlen来表示整个区间(答案具有单调性,短的个数一定不会比长的个数少),进行拓扑排序。

这里的拓扑排序,可以用基数排序来实现,因为节点的父子关系体现在 m a x l e n maxlen maxlen上面就是 m a x l e n maxlen maxlen的单调性,对 m a x l e n maxlen maxlen排序然后操作就可以了。(用到了 S A SA SA的一些思想)

对于多模式串,也可以用 S A SA SA的思想进行建SAM。

#include<cstdio>
#include<iostream>
#include<cstring>
#define For(aa, bb, cc) for(int aa = (bb); aa <= (int)(cc); ++aa)
#define Forr(aa, bb, cc) for(int aa = (bb); aa >= (int)(cc); --aa)
#define Cpy(aa, bb) memcpy(aa, bb, sizeof bb)
using namespace std;
const int maxn = 1e6 + 10;
int len;
char s[maxn];
int val[maxn << 1];
int num[maxn << 1], id[maxn << 1], cnt[maxn << 1], ans[maxn];

//sam

int maxlen[maxn << 1];
int trans[maxn << 1][26];
int link[maxn << 1];
int lst, tot;

void init(){
	lst = tot = 1;
}

void extend(int c){
	int now = ++tot, p;
	val[now] = 1;
	maxlen[now] = maxlen[lst] + 1;
	for(p = lst; p && !trans[p][c]; p = link[p]) trans[p][c] = now;
	if(!p) link[now] = 1;
	else{
		int q = trans[p][c];
		if(maxlen[q] == maxlen[p] + 1) link[now] = q;
		else{
			int sp = ++tot;
			maxlen[sp] = maxlen[p] + 1;
			Cpy(trans[sp], trans[q]);
			link[sp] = link[q];
			for(; p && trans[p][c] == q; p = link[p]) trans[p][c] = sp;
			link[q] = link[now] = sp;
		}
	}
	lst = now;
}

void topsort(){
	For(i, 1, tot) ++num[maxlen[i]];
	For(i, 1, len) num[i] += num[i - 1];
	For(i, 1, tot) id[num[maxlen[i]]--] = i;
	Forr(i, tot, 1) val[link[id[i]]] += val[id[i]];
}

int main(){
#ifndef ONLINE_JUDGE
	freopen("in.txt", "r", stdin);
	freopen("out.txt", "w", stdout);
#endif
	ios::sync_with_stdio(false);
	cin.tie(NULL);
	init();
	cin >> (s + 1);
	len = strlen(s + 1);
	For(i, 1, len) extend(s[i] - 'a');
	topsort();
	For(i, 1, tot) ans[maxlen[i]] = max(ans[maxlen[i]], val[i]);
	Forr(i, len - 1, 1) ans[i] = max(ans[i], ans[i + 1]);
	For(i, 1, len) printf("%d\n", ans[i]);
	return 0;
}

UDP:2021.8.4
在hdu多校第四场里面碰到了一道SAM可做的题:
第四场4–Display Substring
题意:给你个字符串,以及26个字母每个字母权值,一个子串的权值为淄川中每个字符的权值和。求第k大的子串( n < = 1 e 5 n<=1e5 n<=1e5 && k < = 1 e 12 k <= 1e12 k<=1e12)

个人思考:
当时初看这道题的时候,觉得可以和以前做过的一道题联系起来–求字典序第k大(在以前的博客里面有)。然后想采用权值排序后的序为其新的序,再采用那道题的做法。但是这里带来了一个问题,字典序的题目的序是越靠前的优先级越高,也就是先选小再选大的(如1+5)是比先选大的再选小的(如2 +3)小。而这道题里面序只与权值大小相关,与顺序无关。

考虑怎么去解决这个问题。首先直接判断每个子串的大小然后进行排序,这样直接做的复杂度是不可接受的。不妨换个思路,先二分答案,然后再判断有多少个子串的权值在这个范围内。接着来考虑如何解决这个问题:

首先明确一点,对一个字符串 S [ 1... i ] S[1...i] S[1...i]的后缀,后缀长度越短,权值就会越小。而要考虑到所有的子串,只需要对每个点 i ∈ [ 1 , n ] i \in [1,n] i[1,n],考虑以 i i i为结尾位置的子串,即 S [ 1... i ] S[1...i] S[1...i]的后缀,计算小于等于二分所得答案的后缀有多少个。

但是,这里又引入了一个新的问题,如何区分不同的子串?

SAM提供了一个做法:在OIWiki中的额外信息那里,提到了终点的概念– S [ 1... i ] S[1...i] S[1...i]对应的SAM中的点,可以认为在插入 S [ i ] S[i] S[i]时,新建的那个节点 p p p就是一个终点, p o s [ p ] = i pos[p]=i pos[p]=i,并有以下性质:

(这个性质只是帮助理解下面我们为什么会这么操作)

在link连接形成的树中(即反过来的suffix tree) ,每个节点的终点集合等于其子树内所有终点节点对应的终点的集合。

在此基础上可以给每个节点 p p p 赋予一个最长字符串,是其终点集合中任意一个终点开始往前取 m a x l e n [ p ] maxlen[p] maxlen[p] 个字符得到的字符串。每个这样的字符串都一样,且 m a x l e n [ p ] maxlen[p] maxlen[p] 恰好是满足这个条件的最大值。

(下面的终点不是上面的终点集合,而是终点的定义)
我们考虑SAM中的每个节点都代表了一段连续的后缀子串,且所有点不重复的代表了所有的子串。对每个点而言,这段子串都是这个节点所代表的的最长子串的后缀,长度区间为 [ m a x l e n [ l i n k [ p ] ] + 1 , m a x l e n [ p ] ] [maxlen[link[p]]+1,maxlen[p]] [maxlen[link[p]]+1,maxlen[p]],我们只要知道这个最长的字符串是哪一个,通过二分就可以得到这个点所代表的有多少个是符合要求的。要得到这个字符串代表什么,可以通过终点来得到。由上面的性质可以知道,终点一定代表自己的前缀这个当前最长的字符串。从终点开始沿 l i n k link link边跳跃时,可以得到每个非终点最长的字符串即为终点的长为 m a x l e n [ u ] maxlen[u] maxlen[u]的后缀,这样就可以得到所有的子串中有多少个是符合要求的。且由上面性质可以知道,只要能跳到当前点,任意非终点的长为maxlen的后缀都是该字符串。这样就解决了这道题。

O ( n l o g 2 2 n ) O(n log_2^2{n}^) O(nlog22n)

#include<bits/stdc++.h>
#define For(aa, bb, cc) for(int aa = (bb); aa <= (int)(cc); ++aa)
#define Forr(aa, bb, cc) for(int aa = (bb); aa >= (int)(cc); --aa)
#define Cpy(aa, bb) memcpy(aa, bb, sizeof bb);
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 10;
int n;
LL k;
char s[maxn];
int val[30];

template<int N, int sigma> struct Suffix_Automation{
	int maxlen[N << 1], trans[N << 1][sigma],link[N << 1], lst, tot;
	int sum[N], pos[N << 1]; //pos: terminal point
	int num[N], id[N << 1], f[N << 1]; //id:toposort seq; f:endpos size
	bool is_clone[N << 1]; //clone point
	vector<int> ch[N << 1];

	void init(int Size){
		tot = lst = 1;
		link[1] = 0;
		memset(trans, 0, (Size * 2 + 5) * sigma * sizeof (int));
		memset(pos, 0, (Size * 2 + 5) * sizeof (int));
		For(i, 1, Size * 2 + 5) ch[i].clear();
	}

	void extend(int c){
		int now = ++tot, p = lst;
		f[now] = 1; //endpos Size
		maxlen[now] = maxlen[p] + 1;
		for(; p && !trans[p][c]; p = link[p]) trans[p][c] = now;
		if(!p) link[now] = 1;
		else{
			int q = trans[p][c];
			if(maxlen[q] == maxlen[p] + 1) link[now] = q;
			else{
				int nq = ++tot;
				is_clone[nq] = 1; //clone point
				maxlen[nq] = maxlen[p] + 1;
				pos[nq] = q;
				memcpy(trans[nq], trans[q], sigma * sizeof (int));
				link[nq] = link[q];
				for(; p && trans[p][c] == q; p = link[p]) trans[p][c] = nq;
				link[q] = link[now] = nq;
			}
		}
		lst = now;
	}

	void toposort(){
		For(i, 1, tot) ++num[maxlen[i]];
		For(i, 1, n) num[i] += num[i - 1];
		For(i, 1, tot) id[num[maxlen[i]]--] = i;
		Forr(i, tot, 1) f[link[id[i]]] += f[id[i]];
	}

	void prepare(){
		For(i, 1, n){
			extend(s[i] - 'a');
			sum[i] = sum[i - 1] + val[s[i] - 'a'];
			pos[lst] = i;
		}
	}

	bool check(int x){
		LL cnt = 0;
		For(i, 1, tot){
			int P = pos[i];
			int L = P - maxlen[i] + 1, R = P - maxlen[link[i]];
			while(L < R){
				int mid = (L + R) >> 1;
				if(sum[P] - sum[mid - 1] <= x) R = mid;
				else L = mid + 1;
			}
			if(sum[P] - sum[L - 1] <= x) cnt += P - maxlen[link[i]] - L + 1;
		}
		return cnt >= k;
	}
};

Suffix_Automation<maxn, 26> sam;

int main(){
#ifndef ONLINE_JUDGE
	freopen("in.txt", "r", stdin);
	freopen("out.txt", "w", stdout);
#endif
	int _;
	scanf("%d", &_);
	while(_--){
		scanf("%d%lld", &n ,&k);
		scanf("%s", s + 1);
		For(i, 0, 25) scanf("%d", &val[i]);
		sam.init(n);
		sam.prepare();
		int L = 1, R = sam.sum[n];
		while(L < R){
			int mid = (L + R) >> 1;
			if(sam.check(mid)) R = mid;
			else L = mid + 1;
		}
		if(sam.check(L)) printf("%d\n", L);
		else puts("-1");
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值