之前自己整理过后缀自动机的构造,现在再来整理一波性质。
这个问题非常常见,大概可以根据难易程度分成两种。
这个问题可以用DP来解决,也可以用后缀数组。
DP的效率比较低,后缀数组的话将两个串用分隔符连接起来求出height数组,取sa[i],sa[i-1]分别属于两个串的height的最大值即可。
那么利用后缀自动机该怎么做呢?我们将A串建立后缀自动机,然后用B串在后缀自动机上进行匹配,因为后缀自动机中包含A串的所有子串,并且root到后缀自动机中的任意节点形成的路径都是A的合法子串,所有B串能匹配到的点到根的路径都是AB的最长公共子串。那么我们只要找到所有能匹配的节点到根的最远距离即可。如果匹配到一个点后匹配不上了怎么办呢?后缀自动机中有一个很重要的指针——parent指针,每个点的parent指针指向的是上一个可以接受后缀的节点,即如果当前节点可以接某个后缀那么parent指针指向的节点也一定可以接,可以将parent指针指向的位置表示的状态看成该状态的一个后缀。那么匹配不上我们就不停的跳parent链,直到匹配上或者回到根节点为止。跳到一个节点后当前匹配的长度就变成了l[i](i指可以匹配上的节点)
#include
#include
#include
#include
#include
#define N 200003
using namespace std;
int n,m,ch[N][30],fa[N],a[N],l[N],np,nq,p,q,last,cnt,root;
char s[N],s1[N];
void extend(int x)
{
int c=a[x];
p=last; np=++cnt; last=np;
l[np]=x;
for (;p&&!ch[p][c];p=fa[p]) ch[p][c]=np;
if (!p) fa[np]=root;
else {
int q=ch[p][c];
if (l[p]+1==l[q]) fa[np]=q;
else {
int nq=++cnt; l[nq]=l[p]+1;
memcpy(ch[nq],ch[q],sizeof ch[nq]);
fa[nq]=fa[q];
fa[np]=fa[q]=nq;
for (;ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
}
}
}
int solve()
{
int tmp=0,ans=0;
for (int i=1;i<=m;i++) {
int c=s1[i]-'a';
if (ch[p][c]) p=ch[p][c],tmp++;
else {
while (p&&!ch[p][c]) p=fa[p];
if (!p) p=1,tmp=0;
else {
tmp=l[p]+1;
p=ch[p][c];
}
}
ans=max(ans,tmp);
}
return ans;
}
int main()
{
freopen("a.in","r",stdin);
scanf("%s",s+1);
n=strlen(s+1); last=root=++cnt;
for (int i=1;i<=n;i++) a[i]=s[i]-'a';
for (int i=1;i<=n;i++) extend(i);
scanf("%s",s1+1);
m=strlen(s1+1);
printf("%d",solve());
}
题目链接:code vs 3160 题解戳这里
这个问题的话用后缀数组应该还是可以做的,应该是利用二分答案,稍微麻烦一点。
那后缀自动机呢?还是对第一个串建立后缀自动机,不过这次匹配的时候是多个串进行匹配。每次匹配的时候对于后缀自动机中的每个节点维护一个值h,表示的是到达该节点所能匹配上的最大长度(在构造的时候说过,每个节点都可能是多个节点的儿子,所以从根到该点的路径长度可能是不同的)。光匹配还不够,我们需要按照拓扑序倒序,用每个节点取更新他的parent节点,因为如果匹配到一个状态,那么实际上他parent链上的所有状态都匹配上了, h[fa[i]]=l[i] ( l 表示的是从根节点到该节点的最大距离)。然后对于每个串匹配后得到的每个位置的h数组取min得到数组g,g的最大值极为答案。
PS:所谓的拓扑倒序就是按照L从大到小。
#include
#include
#include
#include
#include
#define N 200005
using namespace std;
int ch[N][30],fa[N],l[N],cl[N],mn[N],ans,a[N];
int n,m,last,root,p,q,np,nq,cnt,v[N],pos[N];
char s[N],s1[N];
void extend(int x)
{
int c=s[x]-'a';
p=last; np=++cnt; last=np;
l[np]=x;
for (;p&&!ch[p][c];p=fa[p]) ch[p][c]=np;
if (!p) fa[np]=root;
else {
q=ch[p][c];
if (l[p]+1==l[q]) fa[np]=q;
else {
nq=++cnt; l[nq]=l[p]+1;
memcpy(ch[nq],ch[q],sizeof ch[nq]);
fa[nq]=fa[q];
fa[q]=fa[np]=nq;
for (;ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
}
}
}
void solve()
{
int tmp=0; p=root;
memset(cl,0,sizeof(cl));
for (int i=1;i<=n;i++){
int c=s[i]-'a';
if (ch[p][c]) p=ch[p][c],tmp++;
else {
while (p&&!ch[p][c]) p=fa[p];
if (!p) p=1,tmp=0;
else tmp=l[p]+1,p=ch[p][c];
}
cl[p]=max(cl[p],tmp);
}
for (int i=cnt;i>=1;i--){
int t=pos[i];
mn[t]=min(mn[t],cl[t]);
if (cl[t]&&fa[t]) cl[fa[t]]=l[fa[t]];
}
}
int main()
{
//freopen("a.in","r",stdin);
int t=0;
memset(mn,127,sizeof(mn));
while (scanf("%s",s+1)!=EOF) {
t++; n=strlen(s+1);
if (t==1) {
root=last=++cnt;
for (int i=1;i<=n;i++) extend(i);
for (int i=1;i<=cnt;i++) v[l[i]]++;
for (int i=1;i<=n;i++) v[i]+=v[i-1];
for (int i=1;i<=cnt;i++) pos[v[l[i]]--]=i;
}
else solve();
}
for (int i=1;i<=cnt;i++) ans=max(ans,mn[i]);
printf("%d\n",ans);
}
题目链接:spoj 1812 题解戳这里
bzoj 2946: [Poi2000]公共串 题解戳这里
这类问题多与拓扑序有一定的关系,我们按照拓扑倒序用每个点取更新fa的取值,就可以得到每个位置后面有多少个子串。然后在后缀自动机上进行dfs,每次从字典序小的开始计算,如果当前子树(说是他的儿子或者一大坨后继更准确,因为不是树的形态)的size>=k就说明第k小的子串的结尾在该子树中,否则k-size,然后从下个继续。有点主席树查询区间k大的意思。
spoj 7258 题解戳这里
bzoj 3998: [TJOI2015]弦论 题解戳这里
bzoj 2882: 工艺 题解戳这里
这类问题一般都与right集合有关系,所谓right集合就是某个状态或者说是子串str在s中每次出现位置的右端点组成的集合,|right|常用来表示right集合的大小。
那么right的大小怎么求呢?right(i)是right(fa[i])的子集,所以我们按照parent树中深度从大到小,依次将每个状态的right集合并入他fa状态的right集合,初始的时候只有主链上的|right|=1
按照这种方式我们不仅可以求出right集合的大小,还可以求出某个子串在字符串中出现的最靠左最靠右的位置等等,再次不在赘述。
需要注意的是每个状态str表示的串的长度是区间(len(fa),len(str)]每个状态str表示的串在原串中的出现次数及出现的右端点相同。也就是每个状态只有一个|right|的意思。
这类问题静态的比较好搞,如果是动态的可能需要借助数据结构进行维护,比如下面的bzoj2555就需要用到LCT动态维护parent树的形态,并维护节点的信息。
poj 1743 Musical Theme 题解戳这里
spoj 8222 NSUBSTR-Substrings 题解戳这里
bzoj 2555: SubString 题解戳这里
近些年新兴的算法。一般有两种形式,一种是对trie建立广义的后缀自动机,另一种是对多个独立的串建立广义的后缀自动机。实现的方式差不多。
对于trie树:我们在只有一个串的时候p就是直接指向前一个字符的位置,因为我们现在是树结构,所以p指向的应该是该点在trie树中父节点的位置,剩下的建立过程与普通的后缀自动机相同。
对于多个串:每次加完一个串就把p回到root。然后自然加入即可。
bzoj 3926: [Zjoi2015]诸神眷顾的幻想乡 题解戳这里
bzoj 2780: [Spoj]8093 Sevenk Love Oimaster 题解戳这里
bzoj 3277: 串 题解戳这里
bzoj bzoj 3473: 字符串 题解戳这里
这类题的重点一般不在后缀自动机,一般后缀自动机只是做预处理用的,关键是DP思路。
bzoj 4180: 字符串计数 题解戳这里
bzoj 4032: [HEOI2015]最短不公共子串 题解戳这里
bzoj 2806: [Ctsc2012]Cheat 题解戳这里
bzoj 3238: [Ahoi2013]差异 题解戳这里
right集合与parent树是后缀自动机最常用也是最好用的两个东西。
right集合一般用来处理计数问题。两者相辅相成不可分离。
有一个比较有用的性质:两个串的最长公共后缀,位于这两个串对应状态在parent树的最近公共祖先上。
bzoj 4516: [Sdoi2016]生成魔咒 题解戳这里
bzoj 4566: [Haoi2016]找相同字符 题解戳这里
bzoj 1396: 识别子串 题解戳这里