听说后缀自动机是一个能帮助你轻松pku的神数据结构?既然这么神奇那没理由不学啊!可我盯着后缀自动机看了一个上午也没有头绪,(主要是看了后面忘了前面,结果后来直接手动模拟一遍就搞清楚怎么跑的了,当时一脸蒙bi啊),然而,我还是决定简要地证明一下后缀机!
参考资料:
https://blog.csdn.net/qq_35649707/article/details/66473069
2012noi冬令营clj讲稿
hzy大佬ppt
*****************************************************************//分界线,哼哼~
对于一个串s,我们令r(zs)(zs为s的子串)表示zs子串在串s所有出现的末位置的集合,令n表示每个子串对应出现的个数,如:
aabbabac(1~8):
r(a)={1,2,5,7},n(a)=4;
r(ab)={3,6},n(ab)=2;
r(ba)={5,7},n(ba)=2;
r(aabba)={5},n(aabba)=1;
r(abba)={5},n(abba)=1……
引理1:对于两个子串s1,s2(length(s1)<=length(s2)),若r(s1)=r(s2),当且仅当s1在s中作为s2的后缀出现。
证明:略~(显然……(其实还是很好想的,实在不懂请回复))
引理2:对于两个子串s1,s2(length(s1) 证明:若s1为s2的后缀,即r(s1)与r(s2)至少有一个公共元素,意味着在其它r(s2)任意位置都出现过s1,但s1可能在其它地方出现,所以r(s2)⊆ r(s1);若s1不为s2的后缀,意味着它们末位置一定不会出现在同一点上,所以r(s1)∩r(s2)=∅ 引理3:对于r集合相同的串,在原串s中出现一定是一段连续区间;但一段连续区间的子串的集合不一定属于同一集合 证明:由引理2得证,显然引理2与引理3是互补的 引理4:我们定义state为一个集合的状态(可以理解为树的节点编号,至于为什么,请往下看。如上例,串aabba与串abba在同一state里),则对于两个状态p,q,我们定义p为q的parent节点,当且仅当p是q的最小真包含的集合(在所有q⊆pi的集合中,n(pi)最小的一个),那么,对于任意一个状态q,有且仅有一个parent节点 证明:存在性显然,现在假设有p1,p2均为q的parent节点,则r(q)⊆ r(p1),r(q)⊆ r(p2),那么r(p1)与r(p2)有公共集合r(q),由引理2发现r(p1)与r(p2)可以合并,则最终只会存在1个parent节点,又因为每个状态有且仅有一个parent节点,所以这k个state状态构成了一颗树,我们姑且称这个树为fail树。 引理5:对于一颗状态树,它的总结点数不会超过2n-1个 证明:等比数列求和~ 首先,每个状态结点的fail指针指向它的parent 引理6:我们再定义maxl(k)为状态k的最长子串大小,同理minl(k)为其最小子串的大小,那么,对于状态p和q,若p为q的父亲,那么maxl(p)=minl(q)-1 证明:显然r(q)⊂ r(p),再次由引理2,我们可以证明minl(q)>maxl(p)……①,现在考虑一个minl(q)-1的串t,显然我们有r(q)⊂ r(t)⊆ r (p),由区间性质得到t∈{T|T=[minl(p),maxl(p)]},所以minl(q)-1<=maxl(p)……②,由①②得到maxl(p)=minl(q)-1 引理7:那么我们发现,一个串s[i],顺着它的fail指针走,能够补全它的所有后缀 证明:显然对于一个状态节点,它会包含长度递减的[minlen,maxlen]的所有串,而此节点的fail指针指向节点的最大长度恰好为此节点minlen-1,因此,最终我们可以不遗漏的走回到初始状态 恩,原理相信都懂了~ 因为我是co的代码,并且对于构树网上都有很多讲解,就直接贴了 1)统计right集合 统计right集合有什么用呢?事实上,我们知道right集合(即上文的r集合)表示的是结尾字符的前缀,如果我们以正确的顺序统计right,我们能表示出以此字符串结尾的出现次数,这样,我们就可以求出以i为长度字符串的最大出现次数。 SPOJ8222重复子串 Description 给定字符串s,定义F(x)表示s的所有长度为x的子串中,重复出现次数最多子串在s中的出现的次数,两次出现可以有部分重叠。 Input 一行,即一个字符串s。注意均为小写字母 Output 共length(s)行,每行一个数。第i行为F(i)。 Sample Input ababa Sample Output 3 2 2 1 1 Hint 1<=length(s)<=250000 思路上文已经给出来了,直接给代码理解: POJ3415公共子串Common Substrings Description 题目大意:你有两个串A,B,给你一个数k,求A与B的公共子串里大于等于k的个数,注意位置不同另算 Input 1 ≤ |A|, |B| ≤ 10^5 1 ≤ K ≤ min{|A|, |B|} Output For the case, output an integer |S|. Sample Input 2 aababaa abaabaa Sample Output 22 通过我和ljr大佬的激烈讨论,我们终于有了思路!这应该算我第一道自己做出来的后缀自动机的题?(别逗了,明明是ljr大佬带你飞的)有大写字母是真的坑!!! 一会儿写思路,先贴代码(因为oj数据水,我们就打纯暴力,听说poj要挂?不过暴力也只是nlog2n?): 这道题主要是加深我们对于r集合的理解,因为不同位置要另算的缘故,我们走到一个状态之后,自然它r集合的所有元素都应该算进去,我们能感性地认识此方法是正确的。 因为对于一个状态,你的每一个转移状态是唯一的,也就是说,当你新匹配了一个字母之后,公共串增加了,我们就得到了一个新的串,显然是要加进去的,这样每一次转移,我们都能得到一个唯一的匹配,并且能将它完整地拼起来,因此不会漏也不会重复。不知道我说清楚没有?如果还不了解请回复 持续更新ing~·fail树
const int N = 2e5+10;
#define FOR(i,L,R) for(register int i=(L);i<=(R);++i)
struct Suffix{
#define MAXNODE N*2//最大结点数
char s[N];//原串
int rt,cnt,last,fal[MAXNODE],maxl[MAXNODE],trans[MAXNODE][130];
//编号,上一个字符,值,fail指针,最大长度,转移(转移状态参照文本标准)
inline void insert(int f){
int v=++cnt;//新建结点
maxl[v]=maxl[last]+1;
int p=last;last=v;//更新
while(p&&!trans[p][f])trans[p][f]=v,p=fal[p];
if(!p)return fal[v]=rt,void();//所有结点均无f转移
int q=trans[p][f];
if(maxl[p]+1==maxl[q])return fal[v]=q,void();//接一个可以做到
int newnode=++cnt;//拆点
maxl[newnode]=maxl[p]+1;//最短的一个
memcpy(trans[newnode],trans[q],sizeof(trans[q]));
fal[newnode]=fal[q];
fal[q]=fal[v]=newnode;
while(p&&trans[p][f]==q)trans[p][f]=newnode,p=fal[p];//如果我有此转移,我的所有fail指针均有此转移
}
inline void build(){
gets(s);//防止有空格
rt=cnt=last=1;
int len=strlen(s)-1;
FOR(i,0,len)insert(s[i]);
}
}
·SAM的应用
现给定字符串s,求F(1),F(2),...,F(length(s)).#include
//思路见下文
#include