为什么叫再谈后缀自动机呢?
因为以前学过一次,但是没有太学懂。
愈发觉得学懂一个算法比草率学很多算法更有用了。
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.具体操作及实现细节
一句话题意:求长度为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;
}