后缀自动机
概要
后缀自动机可以理解为是将字符串所有后缀所建出的 \(Trie\) 进行压缩后得出的 \(DAG\)。
对于一个子串 \(s\),它结束位置的集合称为 \(\text{endpos}(s)\),如 \(aaabbaab\),\(\text{endpos}(ab)=\{4,8\},\text{endpos}(bb)=\{5\}\)。\(s_1,s_2\) 为原串的两个子串,设 \(|s_1| \leqslant |s_2|\),则 \(s_1\) 是 \(s_2\) 的后缀当且仅当 \(\text{endpos}(s_2) \subseteq \text{endpos}(s_1)\),\(s_1\) 不是 \(s_2\) 的后缀当且仅当 \(\text{endpos}(s_1) \cap \text{endpos}(s_2)= \varnothing\)。
由 \(\text{endpos}\) 的包含关系可以得出一个树形结构,称为 \(Parent\) 树:
后缀自动机的节点就是 \(Parent\) 树的节点,每个节点表示一个 \(\text{endpos}\)。
\(SAM\) 的节点数不超过 \(2n-1\),边数不超过 \(3n-4\),数组大小应开成两倍。
\(len:\)为一个 \(\text{endpos}\) 所对应的子串中最长子串的长度。
\(ch:\)为转移函数。
\(fa:\)为后缀连接。
设 \(minlen\) 为一个 \(\text{endpos}\) 所对应的子串中最短子串的长度,得:
后缀自动机是一张有向无环图,其中顶点是状态,而边代表了状态之间的转移。
每一个状态包含了它包含的最长子串的一些连续长度的后缀,不是所有后缀,再短的其他后缀在 \(fa\) 连接的状态,也就是该串的所有后缀在 \(Parent\) 树的链上。
一个字符串的 \(Parent\) 树,是其反串的后缀树。
从初始状态经由任意路径走到某一终止状态,得到的字符串为原串的某一后缀。
从初始状态经由任意路径走到某一状态,得到的字符串为原串的某一子串。
所有终止状态包含了原串的所有后缀,整串状态是终止状态,整串状态在 \(Parent\) 树上的祖先也都是终止状态。
一个状态的 \(\text{endpos}\) 集合大小等于该状态转移到终止状态的方案数。
如 \(abbb\):
构造 \(SAM\) 时有三种情况:
① \(\ aa\ \to \ aab\):
② \(\ aabb\ \to \ aabba\):
③ \(\ aab\ \to \ aabb\):
void init()
{
tot=las=root=1;
}
void insert(int c)
{
int p=las,np=las=++tot;
len[np]=len[p]+1;
while(p&&!ch[p][c]) ch[p][c]=np,p=fa[p];
if(!p) fa[np]=root;
else
{
int q=ch[p][c];
if(len[q]==len[p]+1) fa[np]=q;
else
{
int nq=++tot;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
len[nq]=len[p]+1,fa[nq]=fa[q],fa[np]=fa[q]=nq;
while(ch[p][c]==q) ch[p][c]=nq,p=fa[p];
}
}
}
一个状态包含的子串的出现次数可以先拓扑,再用 \(fa\) 求出,\(siz\) 初值为 \(1\),\(siz_{root}\) 应为 \(0\)。
void calc()
{
for(int i=1;i<=tot;++i) b[len[i]]++;
for(int i=1;i<=tot;++i) b[i]+=b[i-1];
for(int i=1;i<=tot;++i) ord[b[len[i]]--]=i;
for(int i=tot;i;--i)
{
int p=ord[i];
siz[fa[p]]+=siz[p];
}
}
广义后缀自动机,在 \(Trie\) 上建 \(SAM\):
void init()
{
las=tot=root=pre[0]=1;
}
void insert(int c)
{
int p=las,np=0;
if(!ch[p][c])
{
np=las=++tot;
len[np]=len[p]+1;
}
while(p&&!ch[p][c]) ch[p][c]=np,p=fa[p];
if(!p) fa[np]=root;
else
{
int q=ch[p][c];
if(len[q]==len[p]+1)
{
if(np) fa[np]=q;
else las=q;
}
else
{
int nq=++tot;
if(!np) las=nq;
len[nq]=len[p]+1,fa[nq]=fa[q],fa[q]=nq;
if(np) fa[np]=nq;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
while(p&&ch[p][c]==q) ch[p][c]=nq,p=fa[p];
}
}
}
void bfs(int s)
{
queue<int> q;
q.push(s);
fath[s]=0;
while(!q.empty())
{
int x=q.front();
q.pop();
las=pre[fath[x]];
insert(co[x]);
pre[x]=las;
for(int i=head[x];i;i=e[i].nxt)
{
int y=e[i].to;
if(y==fath[x]) continue;
fath[y]=x;
q.push(y);
}
}
}
......
for(int i=1;i<=n;++i)
if(d[i]==1)
bfs(i);
在线的广义后缀自动机,需要两个特判:
int insert(int c,int id)
{
if(ch[las][c]&&len[las]+1==len[ch[las][c]]) return ch[las][c];
int p=las,np=++tot;
len[np]=len[p]+1;
while(p&&!ch[p][c]) ch[p][c]=np,p=fa[p];
if(!p) fa[np]=root;
else
{
int q=ch[p][c];
if(len[q]==len[p]+1) fa[np]=q;
else
{
int nq=++tot;
bool flag=las==p;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
len[nq]=len[p]+1,fa[nq]=fa[q],fa[q]=fa[np]=nq;
while(ch[p][c]==q) ch[p][c]=nq,p=fa[p];
return flag?nq:np;
}
}
return np;
}
......
for(int i=1;i<=n;++i)
{
int lenth;
scanf("%s",s+1),lenth=strlen(s+1),las=1;
for(int j=1;j<=lenth;++j) las=insert(s[j]-'a',i);
}
题目
本质不同子串个数
在 \(DAG\) 上 \(dp\) 求路径条数,或在 \(Parent\) 树上统计 \(len(p)-len(fa_{p})\) 的和。
弦论
求字典序第 \(k\) 大子串,有不同位置的相同子串算作多个和不同位置的相同子串算作一个的两种情况。链接
考虑不同位置的相同子串算作多个的情况,求出 \(\text{endpos}\) 集合大小,在 \(DAG\) 上求出每个节点所能到达的所有节点的 \(\text{endpos}\) 集合大小之和。然后将原串进行匹配,类似平衡树求第 \(k\) 大的方式在 \(SAM\) 上走。
不同位置的相同子串算作一个时,只需将集合大小都赋为 \(1\) 即可。
工艺
求字典序循环同构的最小表示法。链接
将原串倍长,在自动机上贪心地进行匹配。
LCS - Longest Common Substring
求两个字符串的最长公共子串。链接
一个串在另一个上进行匹配,若不能继续匹配,在 \(Parent\) 树上往上跳,去除部分前缀,统计匹配成功的最大值。
LCS2 - Longest Common Substring II
求多个字符串间的最长公共子串。链接
对第一个串建自动机,与求两个字符串的最长公共子串的求法类似,将其他串在上面匹配,求出到每一个节点所能匹配的最大长度,一个节点的贡献是这些最大长度的最小值,答案即为所有节点的贡献的最大值。
也可以用广义后缀自动机求解,记录每个节点有多少个字符串更新,也就是记录每个节点多少个字符串能到达它,若所有字符串都能到达一个节点,那么该节点对答案有贡献。
JZPGYZ - Sevenk Love Oimaster
给定多个模板串,多个查询串,查询每一个查询串是多少个模板串的子串。链接
建广义后缀自动机,将查询串在自动机上匹配,最终到达的节点有多少个模板串更新,即为答案。
Cyclical Quest
给定一个主串和多个询问串,求每个询问串的所有循环同构在主串中出现的次数之和。链接
将询问串倍长,求与主串的最大匹配长度,匹配完后若最大匹配长度大于原询问串长度,则在 \(Parent\) 树上往上跳,删去部分前缀,用最终所到达的节点的 \(\text{endpos}\) 集合大小更新答案。
同时注意去重,对于每个询问串,一个节点只能做一次贡献,打标记即可。
Match & Catch
求两个字符串最短的满足各只出现一次的子串长度。链接
建广义后缀自动机,分别维护两个串的 \(\text{endpos}\) 集合大小。若一个节点的两个集合大小都为 \(1\),就可以去更新答案。
Little Elephant and Strings
给定多个字符串,求每个字符串中有多少个子串满足在这些字符串中出现次数大于等于 \(k\)。链接
建广义后缀自动机,求出每个节点有多少个字符串更新。将每个串在自动机上进行匹配,若当前节点被更新的数量小于 \(k\),就在 \(Parent\) 树上往上跳,删去部分前缀,直到满足要求。
每个节点对答案的贡献为 \(len(p)\),因为若该节点满足被更新的数量大于等于 \(k\),那么该节点中长度最大的子串的所有后缀都对答案有贡献。
Three strings
给定三个字符串 \(A,B,C\),求对于每个长度 \(l\),有多少个三元组 \((a,b,c)\),满足 \(A[a,a+l-1] = B[b,b+l-1] = C[c,c+l-1]\)。链接
建广义后缀自动机,分别维护三个串的 \(\text{endpos}\) 集合大小,一个节点 \(p\) 对区间 \([len(fa_p)+1,len(p)]\) 的贡献为三个串集合大小的乘积,用差分统计答案。
Forensic Examination
给定主串和多个模板串,每次询问主串的子串 \([p_l,p_r]\) 在模板串 \([l,r]\) 中的哪个字符串里的出现次数最多,并求出出现次数。链接
对所有模板串建广义后缀自动机,同时用线段树合并维护出每个节点所对应的子串中,每个模板串的更新次数,同时维护出哪个模板串出现次数最多。
对于子串 \([p_l,p_r]\) 的限制条件,将主串在自动机上匹配,考虑处理每一个 \([1,p_r]\),首先要保证当前匹配长度大于等于 \(p_r-p_l+1\),再在 \(Parent\) 树上倍增往上跳,删去前缀,直到当前节点中恰好有一个串长度为 \(p_r-p_l+1\)。
处理模板串 \([l,r]\) 限制条件时,只需在当前达到的节点所对应的线段树上询问 \([l,r]\) 即可。
Cool Slogans
给定字符串 \(S\),构造字符串序列 \(s_1,s_2,\ldots,s_k\),满足任意 \(s_i\) 都是 \(S\) 的子串,且 \(\forall i\in[2,k]\),都有 \(s_i\) 在 \(s_{i-1}\) 中出现次数大于等于 \(2\),最大化 \(k\)。链接
对 \(S\) 建后缀自动机,用线段树合并维护出 \(\text{endpos}\) 集合。
发现一个性质,若 \(s_i\) 在 \(s_{i-1}\) 中出现的位置为 \(s_{i-1}\) 的中间部分,那么就可以将 \(s_{i-1}\) 的多余的部分后缀删去,删去后并不影响答案,此时 \(s_i\) 为 \(s_{i-1}\) 的一个后缀。
同时发现这个字符串序列肯定是长度递减的,可以翻转过来,求长度递增的序列。
在 \(Parent\) 树上进行 \(dp\),若树上的节点在其向下的节点中出现次数大于等于 \(2\),那么就可以转移。
设一个节点 \(p\) 的往上的祖先最近的成功转移的节点为 \(top\),那么当节点 \(top\) 所对应的 \(\text{endpos}\) 集合在区间 \([ pos(p)-len(p)+len(top),pos(p)-1]\) 上有值时,即可从 \(top\) 向 \(p\) 转移。
取 \(len(p)\) 保证了最优,取 \(len(top)\) 是因为一个节点内的串的效果是等价的。
Security
给定字符串 \(S\),每次询问给定 \(l, r\) 和字符串 \(T\),求字典序最小的 \(s\),\(s\) 为 \(S[l,r]\) 的子串,且 \(s\) 的字典序严格大于 \(T\),求 \(s\)。链接
最优的情况为 \(s\) 除最后一个字符都为 \(T\) 的前缀,最后一个字符的不同使得 \(s\) 的字典序严格大于 \(T\)。
对 \(S\) 建后缀自动机,用线段树合并维护出 \(\text{endpos}\) 集合。
让 \(T\) 在自动机上进行匹配,记录下每次匹配到达的节点,长度从最大匹配由大到小枚举,字符从严格大于 \(T\) 的那一位字符由小到大枚举。
设当前枚举的长度为 \(len\),若之前 \(T\) 在长度为 \(len\) 时匹配到的节点有这个字符的出边,且指向的节点的 \(\text{endpos}\) 集合在区间 \([l+len,r]\) 上有值,那么就找到了答案。
字符串
给定字符串 \(S\),每次询问给定 \(a,b,c,d\),求 \(S[a,b]\) 的所有子串和 \(S[c,d]\) 的最长公共前缀的长度最大值。链接
先二分长度 \(l\),转化为判定 \(S[c,c+l-1]\) 是否在 \(S[a,b]\) 出现。
对 \(S\) 建后缀自动机,用线段树合并维护出 \(\text{endpos}\) 集合。
检验二分时,从 \(S[1,c+l-1]\) 所对应的节点在 \(Parent\) 树上倍增,找到 \(S[c,c+l-1]\) 所对应的节点,再在该节点所对应的线段树上查找 \(\text{endpos}\) 集合在区间 \([a+l-1,b]\) 是否有值即可。
你的名字
给定模板串 \(S\),每次询问给定 \(l,r\) 和一个询问串 \(T\),求在询问串 \(T\) 中有多少个本质不同的子串没有在 \(S[l,r]\) 中出现。链接
将问题转化为求询问串 \(T\) 本质不同的子串个数减去在 \(S[l,r]\) 中出现的本质不同的子串个数
对 \(S,T\) 建分别后缀自动机,线段树合并维护出 \(S\) 的 \(\text{endpos}\) 集合。
让 \(T\) 在 \(S\) 的后缀自动机上进行匹配,设当前的匹配长度为 \(len\),当现在所到达的节点出边所指的节点对应的 \(\text{endpos}\) 集合在区间 \([l+len,r]\) 上有值时,才能进行匹配。
当不能匹配时,缩短当前匹配的长度,这里不能直接在 \(Parent\) 树上向上跳,因为每缩短一个单位长度都可能在区间 \([l+len,r]\) 上有值。
最终答案为 \(\max(len(p)-\max(mtc(pos(p)),len(fa_p)),0)\) 之和。
字符串问题
给定字符串 \(S\),在 \(S\) 的子串中,有 \(n_a\) 个 \(A\) 类串,\(n_b\) 个 \(B\) 类串,\(A\) 类串对 \(B\) 类串存在支配关系,构造字符串序列 \(s_1,s_2,\ldots,s_k\),使得任意 \(s_i\) 都为 \(A\) 类串,且 \(\forall i\in[1,n-1]\),都满足 \(s_i\) 所支配的 \(B\) 类串是 \(s_{i+1}\) 的前缀,求这个字符串序列的总长度最大值。链接
每个 \(A\) 类串向其支配的 \(B\) 类串连边,每个 \(B\) 类串向满足是其前缀的 \(A\) 类串连边,拓扑 \(dp\) 求最长路,若图中有环,则总长度为无穷大。
对 \(S\) 的反串建后缀自动机,那么在 \(Parent\) 树上一个节点所代表的串都是其子树的串的前缀。
对于一个 \(B\) 类串,向其子树中所有的 \(A\) 类串连边,进行优化建图,在同一个节点中按长度从小到大排序后,每个 \(B\) 类串向上一个 \(B\) 类串连边,\(A\) 类串由最后一个长度小于等于其的 \(B\) 类点连边。