后缀自动机的原理和构造:
b站视频:https://www.bilibili.com/video/BV1ez4y117VF
代码blog:https://www.cnblogs.com/xzyxzy/p/9186759.html
先是一个板子题:
后缀自动机 (SAM)
题目描述
给定一个只包含小写字母的字符串S
请你求出 S 的所有出现次数不为 1 的子串的出现次数乘上该子串长度的最大值。
输入格式
一行一个仅包含小写字母的字符串S
输出格式
一个整数,为 所求答案
输入
abab
输出
4
说明/提示
对于10%的数据,|S|<=1000
对于100%的数据,|S|<=10^6
source:https://www.luogu.com.cn/problem/P3804 洛谷P3804
一道板子题,直接根据字符串建立自动机,然后选掉只出现一次的就行,关于解释都在代码里了
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn=2e6+10;
char text[maxn];
int sam[maxn][26],textlen,siz[maxn],link[maxn],len[maxn],nodecnt=1,cur,last=1,temp[maxn],sa[maxn];
void insert(int c)
{
int k=last;
cur=++nodecnt;
last=cur;
len[cur]=len[k]+1;
siz[cur]=1;
while(k&&(!sam[k][c]))
{
sam[k][c]=cur;
k=link[k];
}
if(!k)//退出的时候走到了根节点对应第一种插入情况
link[cur]=1;
else if(len[sam[k][c]]==len[k]+1)//第二种情况
link[cur]=sam[k][c];
else//第三种
{
int clone=++nodecnt,x=sam[k][c];
len[clone]=len[k]+1;
link[clone]=link[x];
link[x]=link[cur]=clone;
memcpy(sam[clone],sam[x],sizeof(sam[clone]));
while(k&&sam[k][c]==x)
{
sam[k][c]=clone;
k=link[k];
}
}
}
int main()
{
int T;
ll K,ans=0;
scanf("%s",text);
textlen=strlen(text);
for(int i=0;i<textlen;i++)//建立后缀自动机
insert(text[i]-'a');
//下面是对自动机的处理,size代表该endpose类在字符串text中出现的次数
//为了求出每个节点对应的size值,要对link树用dfs序,每个子树的根节点的size是该子树所有节点的size总和
//叶子节点的size必然是1,因为叶子说明在加入当前字符的时候该节点的len是最长的,在字符串text里面只出现了一次
//为了简化对link树的反转工作,这里以len从小到大对节点排序,再从大到小遍历,就可以实现dfs的功能,排序方法是桶(基数)排序
for(int i=1;i<=nodecnt;i++)
temp[len[i]]++;
for(int i=1;i<=nodecnt;i++)
temp[i]+=temp[i-1];
for(int i=nodecnt;i>=1;i--)//这里从1到nodecnt也可以,不像后缀数组,这里不要求要稳定(不知道为啥)
sa[temp[len[i]]--]=i;
for(int i=nodecnt;i>=1;i--)
{
siz[link[sa[i]]]+=siz[sa[i]];//这就是实现dfs功能的精髓,因为先遍历到len较大的,是在最后插入的,就是先处理叶子,在处理枝和根
if(siz[sa[i]]>1)
ans=max(ans,1ll*siz[sa[i]]*len[sa[i]]);
}
printf("%lld\n",ans);
return 0;
}
然后再来一道也比较简单的题
弦论
题目描述
为了提高智商,ZJY 开始学习弦论。这一天,她在《String theory》中看到了这样一道问题:对于一个给定的长度为 n 的字符串,求出它的第 k 小子串是什么。你能帮帮她吗?
输入格式
第一行是一个仅由小写英文字母构成的字符串 s。
第二行为两个整数 t 和 k,t为 0 则表示不同位置的相同子串算作一个,t 为 1 则表示不同位置的相同子串算作多个。k 的意义见题目描述。
输出格式
输出数据仅有一行,该行有一个字符串,为第 k 小的子串。若子串数目不足 k 个,则输出 −1。
输入输出样例
输入 #1
aabc
0 3
输出 #1
aab
输入 #2
aabc
1 3
输出 #2
aa
输入 #3
aabc
1 11
输出 #3
-1
说明/提示
数据范围
对于 10% 的数据, n≤1000。
对于 50% 的数据,t=0。
对于 100% 的数据,1≤n≤5×1e5,0≤t≤1,1≤k≤1e9。
source:洛谷P3975
思路:先把文本建个后缀自动机,然后更具T为0或1判断一下,如果是1那size就没啥问题,如果是0,那么size就都赋值为1,因为不同位置的子串只算一个
然后题目要求第k小子串,我们可以考虑size的性质,是该endpose含有的maxleng串的合法位置个数(这里合法就指的是如果T是零,那么不管位置有几个都是1)那么怎么求第k小串呢,我现在写博文的时候觉得后缀数组是不是更好一点呢?还是说自动机咋搞,因为后缀自动机的性质,任意子串都可以由从根节点出发的一个子串得到,任意从根节点出发的一条路径对于一个子串,那么好了,每一个子串都有一条路径与之对应,那么每个节点原本代表一个endpose类,现在只让他代表endpose里面maxleng的子串,然后统计一下以该节点为根的自动机(DAG有向无环图)(不是link树)里面由多少合法字符串(以该节点为根的DAG对应的子串都已该根的maxleng串为前缀),存一下,遍历的时候对于每个正在被访问的节点从a到z遍历,看应该落在那个树上,然后打印该边的字符,在进去这个边的子树接着找就行
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include<iostream>
#include<algorithm>
//using namespace std;
typedef long long ll;
const int maxn=1e6+10;
char text[maxn];
int sam[maxn][26],textlen,size[maxn],link[maxn],len[maxn],nodecnt=1,cur,last=1,temp[maxn],sa[maxn],sum[maxn];
void insert(int c)
{
int k=last;
cur=++nodecnt;
last=cur;
len[cur]=len[k]+1;
size[cur]=1;
while(k&&(!sam[k][c]))
{
sam[k][c]=cur;
k=link[k];
}
if(!k)
link[cur]=1;
else if(len[sam[k][c]]==len[k]+1)
link[cur]=sam[k][c];
else
{
int clone=++nodecnt,x=sam[k][c];
len[clone]=len[k]+1;
link[clone]=link[x];
link[x]=link[cur]=clone;
memcpy(sam[clone],sam[x],sizeof(sam[clone]));
while(k&&sam[k][c]==x)
{
sam[k][c]=clone;
k=link[k];
}
}
}
void quary(int u,int k)
{
if(k<=size[u])
return;
k-=size[u];//记得把该店对应的合法字符串数目剪掉
for(int i=0;i<26;i++)
{
int t=sam[u][i];
if(!t)//如果没有这个边就跳过
continue;
if(k>sum[t])//大了说明不在该子树里面
{
k-=sum[t];
continue;
}
putchar(i+'a');//说明就在该子树里面
quary(t,k);
return;
}
}
int main()
{
int T;
ll K;
scanf("%s",text);
scanf("%d %lld",&T,&K);
textlen=strlen(text);
for(int i=0;i<textlen;i++)
insert(text[i]-'a');//建立后缀自动机
for(int i=1;i<=nodecnt;i++)//用桶排序计算size
temp[len[i]]++;
for(int i=1;i<=nodecnt;i++)
temp[i]+=temp[i-1];
for(int i=nodecnt;i>=1;i--)
sa[temp[len[i]]--]=i;
for(int i=nodecnt;i>=1;i--)
size[link[sa[i]]]+=size[sa[i]];
for(int i=1;i<=nodecnt;i++)
T?(sum[i]=size[i]):(sum[i]=size[i]=1);//这就是看T来修正size
size[1]=sum[1]=0;
for(int i=nodecnt;i>=1;i--)//看一下该节点为根的自动机上由多少节点,然后存进sum
for(int j=0;j<26;j++)
if(sam[sa[i]][j])
sum[sa[i]]+=sum[sam[sa[i]][j]];
if(sum[1]<K)
printf("-1\n");
else
{
quary(1,K);
printf("\n");
}
return 0;
}
2021.8.17