字符串
KMP
Example1(zr23省选第一轮集训day5b)
必须提一下的是,能用KMP的前提是可以比较两个字符串是否相等,不一定是比较两个字母相等.只要能比较两个字符串是否相等,并且已知
因此,我们取
Example2(NOI2014 动物园)
自然的想法是先求border数组,然后每次暴力跳border直到当前前后缀不重叠.根据border的定义显然是对的.但这样复杂度不对.
另一个想法是我们能不能在做KMP的时候,直接判掉当前长度是否超过,如果超过就放弃呢?也不对,因为这样往前跳也会用到这个border数组,但往前跳有可能跳的很少.
因此我们先求border,再第二遍做KMP,用另一个数组,但是往前跳的时候用border跳,其他情况正常做就行.
border理论
定理
定理1
证明显然.
定理2(周期引理)
弱周期引理:如果
考虑分开两面证明,不妨设
由于
强周期引理:如果
这个有一个生成函数证明.简单来说我们不妨设长度为
如果我们能说明
此时注意到括号里面的那个东西的次数有限,设其为
定理3
若
定理4
若
证明考虑WPL就行.
定理5
不妨设最长的border长度为
定理6
一个串的所有border按照长度排序后,可以被划分成
首先,将该串的长度
再考虑最小循环节
Example
Example1([POI2011]OKR-Periodicity)
注意到一个事实:如果这个字符串存在长度为
考虑从小周期开始向大周期确定,首先可以用KMP求出所有前缀的最大border,然后就可以得到整个字符串的所有border.换句话说,我们实际上是在一步一步确定整个字符串的若干前缀的最大border.
考虑border理论,设
如果
为什么这样一定是对的呢?我们考虑什么时候全
-
新增一个长度
的border, :考虑 的最后一段是一段全 ,也就必然意味着 的最后一段是全 ,这么不断推下去就可以说明整个序列都是全 ,此时放上 必定合法. -
新增一个长度
的border, :不妨设当前的 是最大的那个(最小的无意义,因为需要保证 ),此时最短周期必然是 .由于 也是周期并且二者之和 ,因此必然有 .把 按照 长度划分.如果 必有该串是全 串,不然考虑此时 , 是 的一段后缀.考虑此时的周期必然 ,首先不可能等于,如果大于的话可以平移一格.不妨假设周期比 少了 ,那么此时必定有 的前 个字符是 ,但是由于 后面第一个 也往前平移了 格,因此它的第 个字符必定是 ,这就保证了 必定合法.
SA
Example1
给定一个长度为
不难发现一定有一组答案每段的长度是
那么怎么优化呢?我们考虑类似height的证明:
ACAM
用于对于每个文本串的前缀,求出它以哪些模式串为后缀.
Example1(uoj772企鹅游戏)
考虑一个暴力:建出
但是这个复杂度是正确的.
为啥呢?首先对于任意节点,它在fail树上的祖先中匹配节点个数不可能超过
但还没完,考虑所有长度小于等于
Example2(loj3396 novel)
offline.
PAM
引理
- 本质不同回文串最多只有
个.
证明:考虑类似manacher,每次将
算法
回文自动机由转移边和fail树构成,经过一条转移边的影响是在前后均添加一个该字符,一个状态在fail树上指向它的最长回文border.
我们记录两个根:长度为
增量构造,每次加入个新字符,然后在fail树上跳祖先直到
继续跳这个节点,直到又遇到一个位置,那这个位置就是当前节点的fail指针所指向的点.这个操作是
SAM
-
表示子串 在 中出现位置的末尾集合,特别地,我们设 . -
若两个不同的子串的
相等,则称它们为一个 等价类.
下面开始证明引理:
引理
-
字符串
的两个非空子串 和 的 相同(假设 ),当且仅当字符串 在 中的每次出现,都是以 后缀的形式存在. -
字符串
的两个非空子串 和 的 集合的交为空(假设 ),当且仅当字符串 不是 的后缀. -
字符串
的两个非空子串 和 的 集合的交为 (假设 ),当且仅当字符串 是 的后缀.
证明都是显然的.
- 对于一个
等价类中的子串 ,要么 是这个等价类中最短的子串,要么存在一个子串 且 , 是 的后缀
由前面的引理,容易证明.
- 对于一个
等价类中最短的子串 ,不妨设 是 去掉最前面的元素后得到的子串,那么 在另一个 等价类中,我们将 ,记 或 ,这就是后缀链接link,这些关系构成树.
首先除了
等价类的数量有 个.
考虑后缀链接树,显然一个点的
约定
-
记
为 这个 等价类中最长的一个字符串,记 为它的长度.类似地定义 和 ,不难发现 .每个节点的子串数量也就是 . -
记
为 这个 等价类的 集合的大小.
算法
先来捋一下整个过程:整个SAM分为两部分:
第一部分:后缀链接树(parent tree).
它的信息由下文中的fa记录.对于每一个节点:它对应一个endpos等价类,因此它拥有一个父亲节点,也就是后缀链接link指向的节点.同时它拥有一个len,表示这个endpos等价类中最长的子串的长度.
第二部分:trie图.
它的信息由下文中的son记录,表示一个endpos(设为x)通过一条trie边走到另一个endpos(设为y),不难发现x中的所有endpos+1所形成的集合包含y.我们注意这一点后,会发现只要从
也就是说,走trie边的过程是不断在字符串后面添加字符的过程,而走link的过程是不断在字符串前面删去字符串的过程(当然,反向link自然是不断在字符串前面加上另一个字符串集合的过程).
下面给出构造代码.
struct SAM{
int fa;
int son[27];
int len,siz;
}tr[MAXN<<1|1];
int cntp=1,las=1;
int End[MAXN];
inline void add_c(int c,int i){
int x=++cntp;
End[i]=x;tr[x].siz=1;//新建一个endpos={i}
//End[i]存的是前缀[1,i]的结束位置,由于我们当前正在插入i,自然是x.
int prex=las;
las=cntp;
//las存储的是当前的终止节点,其实也就是End[i],我们每次要找到上一次的终止节点,根据它来操作.
tr[x].len=tr[prex].len+1;
for(;prex&&tr[prex].son[c]==0;prex=tr[prex].fa)tr[prex].son[c]=x;
//考虑当前的串:[1,i-1]+'c',如果前面存在一个endpos集合包含[j,i-1](这个集合可能是空子串所在的集合)并且它存在一条'c'边,那么就存在这么一个子串[j,i-1]+'c',它的endpos应该是{i}这个集合的祖先.
//如果在判断[j,i-1]的时候,发现[j,i-1]+'c'在原串中不存在,那么我们就直接连过来.可以发现在这个跳跃的过程中就是不断探索当前x的shortest的过程.
if(!prex){
//说明一直到最后都没有找到字母c,这也意味着c在前面根本没出现过,于是endpos={i}的等价类是[1,i],[2,i],...,[i,i],所以父亲设为1.
tr[x].fa=1;
return ;
}
int y=tr[prex].son[c];
//考虑这里的prex到底是什么意义,它意味着我们找到了一个最长的[j,i-1]的子串所在的endpos集合,并且[j,i-1]+'c'这个子串在原串存在,这也意味着[j,i-1]+'c'这个子串所在的endpos集合必然真包含{i},而这个子串的长度是tr[prex].len+1.
if(tr[y].len==tr[prex].len+1){
//如果y这个节点的长度恰好也是tr[prex].len+1,那么必然意味着[j,i]这个子串完全就在y这里,而[j-1,i]这些子串不在y这里,但被y表示的endpos集合包含.
tr[x].fa=y;
}
else {
//反之,这里y这个节点的endpos集合就可以分成两部分了:第一部分的endpos集合在加入[j,i]这个子串后不变:因为它们的长度都大于tr[prex].len+1,它们必然不可能存在一个endpos是i.而第二部分,其实也只包含一个子串:就是长度等于tr[prex].len+1的子串,它的endpos集合必然是第一部分的endpos集合并上{i},根据我们上面所发现的parent tree的本质是合并endpos集合的性质,它应该是第一部分以及{i}的父亲(也就是这两部分合并的结果),我们把第二部分拿出来单独建点.
int fay=++cntp;
tr[fay]=tr[y];
tr[fay].len=tr[prex].len+1;
tr[y].fa=tr[x].fa=fay;
for(;prex&&tr[prex].son[c]==y;prex=tr[prex].fa)tr[prex].son[c]=fay;
//注意单独建点后,原本指向y的trie边要改向.这是为什么呢?考虑当前这条边是什么意义:它必然指向一个endpos集合要包含{i}的点,因为这样才能保证trie图的性质.此时指向y的点就不能是包含{i-1}的点了.
}
return ;
}
int que[MAXN<<1],l,r;
int ind[MAXn<<1];
inline void work_siz(){//通过一次拓扑排序处理出siz
l=1,r=0;
for(int i=1;i<=cnt;++i){
++ind[tr[i].fa];
}
for(int i=1;i<=cnt;++i){
if(ind[i]==0)que[++r]=i;
}
while(l<=r){
int x=que[l];++l;
tr[tr[x].fa].siz+=tr[x].siz;
--ind[tr[x].fa];
if(ind[tr[x].fa]==0)que[++r]=tr[x].fa;
}
return ;
}其实还是省掉了很多说明:比如这里的复杂度证明以及边数证明,但是我们咕了吧.
应用
检查字符串是否出现
从根开始跳trie边就行.
不同子串个数
显然是
例题
Example1
给出一个长度为
-
是原串的子串. -
每次
在原串中作为子串出现后,要么紧跟着出现一个子串 ,要么 后面放不下一个子串 .
两个字符串被认为是不同的当且仅当他们在某个位置上字母不同,
首先,我们考虑确定
-
的endpos集合是 的endpos集合的后缀. -
的endpos集合中存在的最大的不存在于 的endpos集合的endpos的大小小于 .
看到这里你可能有疑问:为啥要建立反串.因为不建立反串的话
那么接下来我们要在SAM上判断这两件事,我们需要一些更方便判断的条件.首先一个自然的发现是,
接下来就拆重链,写单调栈就行.
评论