题目描述
Description
Kevin是一个热爱字符串的小孩。有一天,他把自己的微信登录密码给忘记了,万般无奈之下只好点“找回密码”。
这时候,网页上出现了当初设定的密保问题:在字符串st中,有若干个内容不同的子串,请问其中字典序第k小的子串是什么?
很可惜的是,Kevin现在已经不会写程序了,所以,他找到了睿智的你来帮忙。
Input
输入数据包括两行:第一行为字符串st,第二行为正整数k,定义如题目描述。
其中字符串st的长度不超过100,000且只由大小写英文字母组成
Output
一行,为第k小的字符串,如果字符串st中不足k个不同的子串,则输出字典序最大的一个。
Sample Input
AAB
2
Sample Output
AA
Data Constraint
50%的数据,|st| <=1000
100%的数据,|st|<=100,000,K < 2^63
50%
暴力枚举每个串,放在一起排序去重
100%
显然,这样做会发现有很多重复的串
所以需要一种算法来不重复记下一个串所有的子串
于是便有了后缀自动机SAM (Suffix Automaton)
SAM
(本题的正确代码在最下面)
假的SAM就留作反面教材
SAM实质上就是用 O(n) O ( n ) 级别的时间/空间来记录下一个串所有的子串(共 n2 n 2 个)
最简单的方法当然是存下一个串所有的后缀,然后每个后缀的前缀就是一个子串。
那么直接把它放在一个trie里就可以做到不重复
例如:aabbabd
但是这样做的话节点数太多了,有
n2
n
2
个
所以这就是SAM的精髓所在——合并状态。
合并状态
仔细观察发现,有不少子串在原串出现的结尾位置集合相同,那么可以把这些串缩成一个点
然后从起点到这个点的任何一条路径所代表的都是这些子串
就以aabba为例
如图,4号节点所代表的子串就是aabb、abb和bb。
而它们在原串的结尾位置集合也一样,都是{4}
同理,4号节点不能代表子串a和b,因为它们的集合是{1,2,5}和{3,4}
把具有相同集合的子串合并起来,就是SAM的关键所在。
如果一些子串的集合相同,那么其中的一些(个)必定是另一些(个)的后缀
意会
fail指针(图上的蓝色边)
实际就是网上所说的parent树,但实质跟其它自动机的定义差不多。
fail[x]表示
x的集合∈fail[x]的集合且|fail[x]的集合|最小
比如上图4节点的集合是{4},而fail[4]=5(b)的集合是{3,4}
简单来讲,就是fail[x]代表的节点都是x代表的节点的后缀
(学过AC和回文自动机的话这里应该很好理解)
至于为什么要搞fail,大概是在新建节点时,让每一个能接的上子串都接上
len数组(图上的红色数字)
就是从起点到每个点的最长串长度
很好求,每次新建节点时+1
用处先猜
next数组(图上的黑色边)
next[x][y]表示节点x接上y后所代表的状态(编号)
(实际上跟其它自动机差不多,只不过SAM一个点代表了多个串)
建自动机
按顺序,依次加上新节点。
每次先在len最长的节点加上一个点x(因为没有能代表新串的节点,所以要新建),
之后顺着fail链往上跳。
设加上的字符为y,当前节点为now
那么可以分几种情况:
①next[now][y]=0
既然所属节点没有y的转移,那么可以直接接上
即next[now][y]=x
②next[now][y]≠0
设z=next[mow][y]
先列出来,等下再解释
1、len[z]=len[now]+1
那么直接把fail[x]设为z
2、len[z]>len[now]+1
把z克隆一遍,再把now及其祖先所以指向z的next边都指向新节点,
新节点继承 z的fail和next边
z和x的fail指向新节点
原因
如果next[now][y]≠0,那么也就是说可能可以直接接上去。
因为now是x的后缀,now+y 可能 是x的后缀
但直接接上可能会出现这样的情况:
看图,节点4的串是aabb abb bb,节点3的串是aab ab b
显然bb不是aab的后缀。
原因是因为S->3之间还有1和2两个节点,尽管S是3的前缀,但是因为中间有1和2,导致S+b 不能 和3+b共享后缀
所以必须要单独开一个代表S+b的节点,这样才能把后缀接上去
如图,因为S+b到的3不是4的后缀,所以必须单独把S+b提出来
而新的节点继承了老节点的所有信息,所以从起点到现在的5就相当于到3了
So可以把 所有 now的祖先所指向z的点都指向新节点
至于为什么跳fail就能把所有的边都改过来,原因如下:
如果两个点a和b都有一条连向z的边,那么显然a+y和b+y出现的集合相同。
那么a和b中其中一个必然是另一个的后缀(或ab的集合也相同)
所以顺着fail链往上跳就行了。
如果now和z只有一字之差(即len[z]=len[now]+1),那就可以直接接上去
因为next[x]能接上去的点都不同
意会
(2018年5月5日添加)
重新看了一遍自己写的东西发现差点看不懂
因为next[x]接上去的点不同,所以now和z之间一定只有一条边
其实可以手推一下,可以发现如果新加一个节点,那么这个节点一定跟原节点一模一样
所以没有必要新加节点
(2018年7月13日添加)
发现自己又写了假的SAM。。。
计算次数应该先加在last上,最后按照len排序,从儿子加到父亲
复制节点时不用复制出现次数,因为最后可以顺着fail加上去
过程
看这里
https://www.cnblogs.com/oyking/archive/2013/08/02/3232872.html
例题
题目描述
洛谷P3804 【模板】后缀自动机
题目描述
给定一个只包含小写字母的字符串S,
请你求出S 的所有出现次数不为 1的子串的出现次数乘上该子串长度的最大值。
输入输出格式
输入格式:
一行一个仅包含小写字母的字符串
S
输出格式:
一个整数,为 所求答案
输入样例
abab
输出样例
4
题解
裸题,每次顺着后缀往上累加。
伪·code
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#define fo(a,b,c) for (a=b; a<=c; a++)
#define fd(a,b,c) for (a=b; a>=c; a--)
#define max(a,b) (a>b?a:b)
using namespace std;
int next[2000010][26];
int sum[2000010];
int fail[2000010];
int mx[2000010];
int i,j,k,l,Len,len,last,ans,I,J,K,L;
char s[1000010];
int d[1000010];
void New(int x,int y)
{
next[x][y]=++len;
mx[len]=mx[x]+1;
}
int main()
{
len=1,last=1;
mx[1]=0;
sum[1]=1;
fail[1]=0;
scanf("%s",s);
Len=strlen(s)-1;
fo(i,0,Len)
{
New(last,s[i]-'a');
K=last;
last=next[last][s[i]-'a'];
for (j=fail[K]; j; j=fail[j])
if (!next[j][s[i]-'a'])
next[j][s[i]-'a']=last;
else
{
if ((mx[j]+1)==mx[next[j][s[i]-'a']])
fail[last]=next[j][s[i]-'a'];
else
{
k=next[j][s[i]-'a'];
New(j,s[i]-'a');
sum[len]=sum[k];
fo(l,0,25)
next[len][l]=next[k][l];
fail[len]=fail[k];
fail[last]=len,fail[k]=len;
for (l=j; l; l=fail[l])
{
if (next[l][s[i]-'a']==k)
next[l][s[i]-'a']=len;
}
}
break;
}
if (!j)
fail[last]=1;
for (j=last; j; sum[j]++,j=fail[j]);
}
ans=0;
fo(i,2,len)
if (sum[i]>1)
ans=max(ans,mx[i]*sum[i]);
printf("%d\n",ans);
}
例题*2
题目描述
Description
Kevin是一个热爱字符串的小孩。有一天,他把自己的微信登录密码给忘记了,万般无奈之下只好点“找回密码”。
这时候,网页上出现了当初设定的密保问题:在字符串st中,有若干个内容不同的子串,请问其中字典序第k小的子串是什么?
很可惜的是,Kevin现在已经不会写程序了,所以,他找到了睿智的你来帮忙。
Input
输入数据包括两行:第一行为字符串st,第二行为正整数k,定义如题目描述。
其中字符串st的长度不超过100,000且只由大小写英文字母组成
Output
一行,为第k小的字符串,如果字符串st中不足k个不同的子串,则输出字典序最大的一个。
Sample Input
AAB
2
Sample Output
AA
Data Constraint
50%的数据,|st| <=1000
100%的数据,|st|<=100,000,K < 2^63
题解
没错就是一开始的题目(滑稽)
建出自动机后统计从每个点开始走能构成的字符串总数(dfs一遍),
之后直接找就行了
实际90分才是正解
code
//jzoj4025 Sam
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#define fo(a,b,c) for (a=b; a<=c; a++)
#define fd(a,b,c) for (a=b; a>=c; a--)
#define max(a,b) (a>b?a:b)
using namespace std;
int next[200010][52];
int fail[200010];
int mx[200010];
int i,j,k,l,Len,len,last,ans,I,J,K,L;
long long sum;
long long f[200010];
char s[100010];
int ls[200010];
int d[200010];
bool bz[200010];
int turn(char ch)
{
if (ch<'[')
return ch-65;
else
return ch-71;
}
int Turn(int s)
{
if (s<26)
return s+65;
else
return s+71;
}
void New(int x,int y)
{
next[x][y]=++len;
mx[len]=mx[x]+1;
}
void find()
{
int i,t;
t=1;
while (sum)
{
if (!ls[t] && !next[t][0])
return;
fo(i,0,ls[t])
if (next[t][i])
{
if ((f[next[t][i]]+1)>=sum || i==ls[t])
{
printf("%c",Turn(i));
sum--;
if (sum)
t=next[t][i];
break;
}
else
sum-=f[next[t][i]]+1;
}
}
}
void init()
{
int h,t,i,j;
h=0,t=1;
d[1]=1;
bz[1]=1;
while (h<t)
{
h++;
fo(i,0,51)
if (next[d[h]][i] && !bz[next[d[h]][i]])
{
bz[next[d[h]][i]]=1;
d[++t]=next[d[h]][i];
}
}
fd(j,t,1)
{
fo(i,0,51)
if (next[d[j]][i])
{
ls[d[j]]=i;
f[d[j]]+=1+f[next[d[j]][i]];
}
}
}
int main()
{
len=1,last=1;
mx[1]=0;
fail[1]=0;
scanf("%s",s);
Len=strlen(s)-1;
fo(i,0,Len)
{
New(last,turn(s[i]));
K=last;
last=next[last][turn(s[i])];
for (j=fail[K]; j; j=fail[j])
if (!next[j][turn(s[i])])
next[j][turn(s[i])]=last;
else
{
if ((mx[j]+1)==mx[next[j][turn(s[i])]])
fail[last]=next[j][turn(s[i])];
else
{
k=next[j][turn(s[i])];
New(j,turn(s[i]));
fo(l,0,51)
next[len][l]=next[k][l];
fail[len]=fail[k];
fail[last]=len,fail[k]=len;
for (l=j; l; l=fail[l])
{
if (next[l][turn(s[i])]==k)
next[l][turn(s[i])]=len;
}
}
break;
}
if (!j)
fail[last]=1;
}
init();
scanf("%lld",&sum);
find();
printf("\n");
}
后记
感觉SAM不是很难啊。。。
然而调了3天
参考资料
https://blog.csdn.net/qq_36551189/article/details/79764070
https://blog.csdn.net/doyouseeman/article/details/52245413
https://blog.csdn.net/YxuanwKeith/article/details/51019000
https://blog.csdn.net/wangzhen_yu/article/details/45481269
https://www.cnblogs.com/oyking/archive/2013/08/02/3232872.html
真·code
#include <algorithm>
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#define fo(a,b,c) for (a=b; a<=c; a++)
#define fd(a,b,c) for (a=b; a>=c; a--)
#define max(a,b) (a>b?a:b)
using namespace std;
int next[2000010][26];
int sum[2000010];
int fail[2000010];
int mx[2000010];
char s[1000010];
int d[1000010];
int b[2000010];
int i,j,k,l,Len,len,last,ans,I,J,K,L;
void New(int x,int y)
{
next[x][y]=++len;
mx[len]=mx[x]+1;
}
void swap(int &a,int &b) {int c=a;a=b;b=c;}
void qsort(int l,int r)
{
int i,j,mid;
i=l;
j=r;
mid=mx[b[(l+r)/2]];
while (i<=j)
{
while (mx[b[i]]>mid) i++;
while (mx[b[j]]<mid) j--;
if (i<=j)
{
swap(b[i],b[j]);
i++,j--;
}
}
if (l<j) qsort(l,j);
if (i<r) qsort(i,r);
return;
}
int main()
{
len=1,last=1;
mx[1]=0;
sum[1]=1;
fail[1]=0;
scanf("%s",s);
Len=strlen(s)-1;
fo(i,0,Len)
{
New(last,s[i]-'a');
K=last;
last=next[last][s[i]-'a'];
for (j=fail[K]; j; j=fail[j])
if (!next[j][s[i]-'a'])
next[j][s[i]-'a']=last;
else
{
if ((mx[j]+1)==mx[next[j][s[i]-'a']])
fail[last]=next[j][s[i]-'a'];
else
{
k=next[j][s[i]-'a'];
New(j,s[i]-'a');//这里不用复制sum
fo(l,0,25)
next[len][l]=next[k][l];
fail[len]=fail[k];
fail[last]=len,fail[k]=len;
for (l=j; l; l=fail[l])
{
if (next[l][s[i]-'a']==k)
next[l][s[i]-'a']=len;
}
}
break;
}
if (!j)
fail[last]=1;
sum[last]++;//先加在最后的节点上
}
fo(i,1,len)
b[i]=i;
qsort(1,len);
fo(i,1,len)
sum[fail[b[i]]]+=sum[b[i]];//最后再累加sum
ans=0;
fo(i,2,len)
if (sum[i]>1)
ans=max(ans,mx[i]*sum[i]);
printf("%d\n",ans);
}