题目描述

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)O(n)级别的时间/空间来记录下一个串所有的子串(共n2n2n^2个)

最简单的方法当然是存下一个串所有的后缀,然后每个后缀的前缀就是一个子串。
那么直接把它放在一个trie里就可以做到不重复

例如:aabbabd

但是这样做的话节点数太多了,有n2n2n^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;elsereturn ch-71;
}int Turn(int s)
{if (s<26)return s+65;elsereturn 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;}elsesum-=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');//这里不用复制sumfo(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]];//最后再累加sumans=0;fo(i,2,len)if (sum[i]>1)ans=max(ans,mx[i]*sum[i]);printf("%d\n",ans);
}

JZOJ4025. 【佛山市选2015】找回密码(后缀自动机SAM)相关推荐

  1. 洛谷P3975【天津省选2015】(后缀自动机DP)

    题目链接 https://www.luogu.com.cn/problem/P3975 题解 此题非常经典且重要,是sam的函谷关,必须拿下. 记录每个点endpos大小的方法是在parent树从下往 ...

  2. 后缀自动机(SAM)讲解 + Luogu p3804【模板】后缀自动机 (SAM)

    本文求节点子串长度最小值有点问题,现已修改. SAM 后缀自动机可以存储某一个字符串的所有子串. 一.概念 下图是一个 字符串 "aababa" 的 后缀自动机. 上图中的 黑色边 ...

  3. 后缀自动机SAM详解

    用一个DFA来识别一个串(比如aabab)的所有后缀,要怎么做呢 最简单的办法,把所有后缀看作要保存的单词,画一棵 trie树,像这样: 点很多很麻烦复杂度也很高 我们给这个DFA按我们的需求合并化简 ...

  4. 多校冲刺NOIP模拟6 - 游戏——矩阵乘法、后缀自动机SAM

    此题不提供链接 题目描述 前言 好久没用SAM了.我记得上次用SAM做题还是在上次. 题解 每一种长度的总方案是确定的,所以我们只需要求出赢的方案数.平局方案数即可. 做法其实和官方正解区别不大,官方 ...

  5. [hdu4416 Good Article Good sentence]后缀自动机SAM

    题意:给出串A和串集合B={B1,B2,...,Bn},求串A的所有不同子串中不是B中任一串的子串的数目. 思路:把A和B中所有字符串依次拼接在一起,然后构造后缀自动机,计算每个状态的R集合元素的最大 ...

  6. 省选专练之后缀自动机SPOJ1811LCS - Longest Common Substring

    陈老师引入SAM的例题 求两个串的LCS 直接建一个串的SAM,并且把另外一个串放进去跳. 这个方法很经典! 几乎满足了所有两个串的公共串问题的所有解法 有巨子说这个跳是均摊Log的,我也不知道QwQ ...

  7. jzoj4024 [佛山市选2015]石子游戏

    Description Alice 和 Bob 总喜欢聚在一起玩游戏(T_T),今天他(她)们玩的是一款新型的取石子游戏.游戏一开始有N堆石子,Alice 和 Bob 轮流取出石子.在每次操作中,游戏 ...

  8. P3804 【模板】后缀自动机 (SAM)

    传送门 文章目录 题意: 思路: 题意: 给你一个字符串sss,让你求sss中出现次数不为111的子串出现次数乘上该字串长度最大值. ∣s∣≤1e6|s|\le 1e6∣s∣≤1e6 思路: 没学明白 ...

  9. 【牛客 - 551C】CSL 的密码(后缀数组,后缀自动机,随机算法)

    题干: 链接:https://ac.nowcoder.com/acm/contest/551/C 来源:牛客网 为了改变这一点,他决定重新设定一个密码.于是他随机生成了一个很长很长的字符串,并打算选择 ...

最新文章

  1. Xilinx zynq的资料获取总结
  2. Android系统中Parcelable和Serializable的区别
  3. Visual Studio安装卸载模板
  4. C Operator | and can also operate bool operands
  5. 创建SSH keys
  6. k8s中service类型
  7. 月入5万,程序员夫人们过上贵妇生活了吗?
  8. 预加载显示图片的艺术
  9. java 泰勒级数_鸡群优化算法(CSO)、蜻蜓算法(DA)、乌鸦搜索算法(CSA)、泰勒级数(Taylor series)...
  10. 关于Unity中的本地存储
  11. ASP.NET操作Excel(终极方法NPOI)
  12. Mybatis 实现关联表查询
  13. Hyper-V Server 存储空间
  14. python switch语句的多种实现方法
  15. 【转】计算机科学中最重要的32个算法
  16. c#代码片段新建(sinppet)
  17. HDU 5908 Abelian Period 可以直接用multiset
  18. ICMP协议解析与实战
  19. 有各组方差怎么算组间平方和_方差分析:组间离差平方和组内离差平方的定义是什么?...
  20. 怎么在win下装python requests

热门文章

  1. 注意力机制 - Transformer
  2. 投中谷歌、亚马逊、推特、Facebook,公认的“互联网风投之王”是如何养成的?
  3. linux系统上传本地文件或文件夹
  4. 使用OPNESSL制作SSL证书
  5. matlab 右边短线,短线选股器
  6. SVM支持向量机-软间隔与松弛因子(3)
  7. 基于STM32单片机的智能水表水流量计原理图PCB
  8. 大学生英语四级备战之③听力
  9. Wopti Utilities 7.74.7.702 注册方法注意事项
  10. 游戏设计的艺术:一本透镜的书——第七章 游戏通过迭代改良