浅谈后缀自动机
后缀自动机
自动机
首先什么是自动机
我们大多用的是\(DFA\),也就是有限状态自动机
整个自动机是由一些边和点组成的,边上的权为字符
简单理解就是输入一个字符串如果是我们想要接受的,那这个字符串就会按顺序遍历图,并最后会在终止节点停下,是为接受
当然,也有一些字符串无法被接受,比如AC自动机就是接受模式串
后缀相关
我们将以\(i\)为结尾的所有子串建一棵\(Tire\)
很明显这样的\(Tire\)点数是\(n^2\)的,同时它可以接受所有子串的功能
而后缀自动机就是最小化上述的自动机
为了缩小点数,我们可以对一些具有相同性质的点缩点
定义\(R_S\)为子串\(S\)在原串中所有出现位置的右端点组成的集合,我们把相同\(R\)的子串缩成一个点
然后探究一下这样做的正确性与性质
首先对于\(R_X\)与\(R_Y\),如果两者有交集,则一定是包含或被包含的关系
考虑反证,如果存在\(R_X,R_Y\)有交集,在交集的位置,我们假设\(|X|>|Y|\),则\(Y\)是\(X\)的一个子串
对于\(pos\in R_X,\notin R_Y\),其\(|Y|\)一定能在\(pos\)位置匹配到,则\(R_Y\subseteq R_X\)
由此,我们定义\(Link\)表示当前节点\(i\)代表\(R\),\(Link_i\)为\(R\subseteq R'\)最小的\(R'\),也就是从\(R'\)这里选一些子集
注意到\(Link\)形成的是一个树的结构,同时每次连边会划分集合至少两个,因此点数保证一定是\(2*n\)左右
注意区分自动机上的连边和\(Link\)边
自动机的连边是在当前子串\(S\)上加一个字符\(ch\)后\(S+ch\)能匹配到所有的右端点,相当于在后面加字符
而\(Link\)边是为了放大\(R\),相当于在前缀删字符最多删多少,同时注意到一个节点代表的是一段连续的区间,因为删字符得到的前缀一定是连续的,如果设\(Len_i\)表示\(i\)点代表字符串的最长长度,那么\(i\)代表的区间为\([Len_{link_i}+1,Len_i]\)
具体实现
后缀自动机的实现是在线的
记当前末尾位置为\(Lp\),字符为\(ch\)
具体的我们考虑记录\(Last\)表示之前自动机末尾停下的节点,\(cur\)为当前末尾的节点,\(cur\)肯定是连在\(Last\)的后面,边为\(ch\),同时\(Len_{cur}=Len_{Last}+1\)
考虑跳\(Last\)字符串的前缀也就是跳\(Link\)链,假设现在跳到了节点\(P\)如果\(P\)没有\(ch\)边,则直接给它连到\(cur\)
找到第一个有\(ch\)边的\(P\),走\(ch\)边后为\(Q\),如果没找到直接\(Link_{cur}=0\)
考虑如果\(Len_Q=Len_P+1\),则\(P\)接\(ch\)等同于\(Q\)就可以了,\(Link_{cur}=Q\),同时\(Q\)的\(Link\)链上的\(R\)全部插一个\(Lp\)(没必要实现)
如果\(Len_Q>Len_P+1\),则\(Q\)对应的子串比\(P+ch\)更长,即在\(Link\)树上\(P+ch\)为\(Q\)的父亲,我们复制\(Q\)到\(Clone\),直接\(Link_Q=Link_{cur}=Clone\),同时我们继续爬\(Last\)的\(Link\)链,将所有指向\(Q\)的全部指向\(Clone\)
struct SAM{
SAM_node Tree[MAXN*2];
int cnt_node;
int Last;
void Build()
{
cnt_node=0;
Last=0;
Tree[0].Link=-1;
Tree[0].Len=0;
return;
}
void Insert(char s)
{
int p=Last;
int cur=++cnt_node;
p=Tree[Last].Link;
Tree[Last].Next[s-'a']=cur;
Tree[cur].Len=Tree[Last].Len+1;
while((p!=-1)&&(!Tree[p].Next[s-'a']))
{
Tree[p].Next[s-'a']=cur;
p=Tree[p].Link;
}
if(p==-1)
{
Tree[cur].Link=0;
}
else
{
int q=Tree[p].Next[s-'a'];
if(Tree[q].Len==Tree[p].Len+1)
{
Tree[cur].Link=q;
}
else
{
int clone=++cnt_node;
Tree[clone]=Tree[q];
Tree[clone].Len=Tree[p].Len+1;
Tree[q].Link=clone;
Tree[cur].Link=clone;
while((p!=-1)&&(Tree[p].Next[s-'a']==q))
{
Tree[p].Next[s-'a']=clone;
p=Tree[p].Link;
}
}
}
Last=cur;
}
}sam;
一些应用
首先明确一些性质
根节点到每个点的所有路径拼起来就是这个点代表的字符串集合
\(R\)的大小就是这个点出现节点的次数,同时\([Len_{Link_i}+1,Len_i]\)是\(i\)代表节点的长度区间
不同字符串的个数及总长度
考虑每个点的贡献分别为\(Len_i-Len_{Link_i}\)和\(\dfrac{(Len_i+1+Len_{Link_i})(Len_i-Len_{Link_i})}{2}\)
出现次数
每个节点的出现次数就是\(|R|\),我们在插入的时候就在终止节点插一个元素,维护即可
K子串
预处理一下当前节点为起点有多少条路径然后贪心即可
#include <bits/stdc++.h>
const int MAXN = 1e6 + 5;
using namespace std;
struct SAM_node {
int Len;
int Next[26];
int Link;
int num;
};
struct SAM {
SAM_node Tree[MAXN * 2];
int cnt_node;
int T;
vector<int>g[MAXN * 2];
vector<int>G[MAXN * 2];
int Rd[MAXN * 2];
long long Dp[MAXN * 2];
int Last;
void Build() {
cnt_node = 0;
Last = 0;
Tree[0].Link = -1;
Tree[0].Len = 0;
return;
}
void Insert(char s) {
int p = Last;
int cur = ++cnt_node;
p = Tree[Last].Link;
Tree[Last].Next[s - 'a'] = cur;
Tree[cur].Len = Tree[Last].Len + 1;
while ((p != -1) && (!Tree[p].Next[s - 'a'])) {
Tree[p].Next[s - 'a'] = cur;
p = Tree[p].Link;
}
if (p == -1) {
Tree[cur].Link = 0;
} else {
int q = Tree[p].Next[s - 'a'];
if (Tree[q].Len == Tree[p].Len + 1) {
Tree[cur].Link = q;
} else {
int clone = ++cnt_node;
Tree[clone] = Tree[q];
Tree[clone].num = 0;
Tree[clone].Len = Tree[p].Len + 1;
Tree[q].Link = clone;
Tree[cur].Link = clone;
while ((p != -1) && (Tree[p].Next[s - 'a'] == q)) {
Tree[p].Next[s - 'a'] = clone;
p = Tree[p].Link;
}
}
}
Last = cur;
Tree[cur].num++;
}
void Link_Build() {
for (int i = 0; i <= cnt_node; i++) {
if (Tree[i].Link != -1) {
g[Tree[i].Link].push_back(i);
}
for (int j = 0; j <= 25; j++) {
if (!Tree[i].Next[j]) {
continue;
}
G[Tree[i].Next[j]].push_back(i);
Rd[i]++;
}
}
}
void dfs(int x) {
for (int i = 0; i < g[x].size(); i++) {
int v = g[x][i];
dfs(v);
Tree[x].num += Tree[v].num;
}
}
void Topsort() {
queue<int>q;
for (int i = 1; i <= cnt_node; i++) {
if (!Rd[i]) {
q.push(i);
}
if (!T) {
Dp[i] = 1;
} else {
Dp[i] = Tree[i].num;
}
}
while (q.size()) {
// printf(">?");
int temp = q.front();
q.pop();
for (int i = 0; i < G[temp].size(); i++) {
int v = G[temp][i];
Rd[v]--;
Dp[v] += Dp[temp];
if (Rd[v] == 0) {
q.push(v);
}
}
}
}
void Rank(int k) {
int p;
for (int i = 0; i <= 25; i++) {
if (!Tree[0].Next[i]) {
continue;
}
int Pox = Tree[0].Next[i];
if (Dp[Pox] < k) {
k -= Dp[Pox];
} else {
printf("%c", i + 'a');
p = Pox;
break;
}
}
while (1) {
// printf("???");
int Kp;
if (!T) {
Kp = 1;
} else {
Kp = Tree[p].num;
}
if (k <= Kp) {
break;
} else {
k -= Kp;
}
for (int i = 0; i <= 25; i++) {
if (!Tree[p].Next[i]) {
continue;
}
int Pox = Tree[p].Next[i];
if (Dp[Pox] < k) {
k -= Dp[Pox];
} else {
printf("%c", i + 'a');
p = Pox;
break;
}
}
}
}
} sam;
string S;
int T;
int k;
int main() {
cin >> S;
sam.Build();
for (int i = 0; i < S.size(); i++) {
sam.Insert(S[i]);
}
scanf("%d %d", &T, &k);
sam.T = T;
sam.Link_Build();
sam.dfs(0);
sam.Topsort();
sam.Rank(k);
}
最小循环移位
首先如果存在循环位移,最小循环节长度应该为\(N-Next[N]\)
证明考虑匹配的是\([1,Next[N]]\)段,你将这一段移一下去正好相等
这启示我们直接建\(S+S\)的后缀自动机,在上面找一个与原串相等且右端点最小的即可(相比于KMP空间有点大)
#include<cstdio>
#include<algorithm>
#include<vector>
#include<iostream>
#include<cstring>
const int MAXN=1e6+5;
using namespace std;
struct SAM_node{
int Len;
int Next[26];
int Link;
int min_num;
};
int Lit;
struct SAM{
SAM_node Tree[MAXN*2];
vector<int>g[MAXN*2];
int cnt_node;
int Last;
void Build()
{
cnt_node=0;
Last=0;
Tree[0].Link=-1;
Tree[0].Len=0;
return;
}
int New()
{
++cnt_node;
for(int i=0;i<26;i++)
{
Tree[cnt_node].Next[i]=0;
}
Tree[cnt_node].Link=0;
Tree[cnt_node].min_num=0x3f3f3f3f;
Tree[cnt_node].Len=0;
return cnt_node;
}
void Insert(char s,int Pos)
{
int p=Last;
int cur=New();
p=Tree[Last].Link;
Tree[Last].Next[s-'a']=cur;
Tree[cur].Len=Tree[Last].Len+1;
while((p!=-1)&&(!Tree[p].Next[s-'a']))
{
Tree[p].Next[s-'a']=cur;
p=Tree[p].Link;
}
if(p==-1)
{
Tree[cur].Link=0;
}
else
{
int q=Tree[p].Next[s-'a'];
if(Tree[q].Len==Tree[p].Len+1)
{
Tree[cur].Link=q;
}
else
{
int clone=++cnt_node;
for(int i=0;i<26;i++)
{
Tree[clone].Next[i]=Tree[q].Next[i];
}
Tree[clone].Link=Tree[q].Link;
Tree[clone].min_num=0x3f3f3f3f;
Tree[clone].Len=Tree[p].Len+1;
Tree[q].Link=clone;
Tree[cur].Link=clone;
while((p!=-1)&&(Tree[p].Next[s-'a']==q))
{
Tree[p].Next[s-'a']=clone;
p=Tree[p].Link;
}
}
}
Last=cur;
if(Pos>Lit)
{
Tree[cur].min_num=min(Tree[cur].min_num,Pos);
}
}
void Link_Build()
{
for(int i=0;i<=cnt_node;i++)
{
g[i].clear();
}
for(int i=0;i<=cnt_node;i++)
{
if(Tree[i].Link!=-1)
{
g[Tree[i].Link].push_back(i);
}
}
}
void dfs(int x)
{
for(int i=0;i<g[x].size();i++)
{
int v=g[x][i];
dfs(v);
Tree[x].min_num=min(Tree[v].min_num,Tree[x].min_num);
}
}
int Find(string S)
{
int P=0;
for(int i=0;i<S.size();i++)
{
P=Tree[P].Next[S[i]-'a'];
}
return Tree[P].min_num;
}
}sam;
char SS[MAXN];
string S;
int T;
int k;
int main()
{
while(1)
{
cin >> SS;
S = string(SS, SS+strlen(SS));
if(S[0]=='.')
{
break;
}
Lit=S.size();
sam.cnt_node=0;
sam.Build();
for(int i=0;i<S.size();i++)
{
sam.Insert(S[i],i+1);
}
for(int i=0;i<S.size();i++)
{
sam.Insert(S[i],(i+1)+(int)S.size());
}
sam.Link_Build();
sam.dfs(0);
int Pd=sam.Find(S);
if(Pd==0x3f3f3f3f)
{
printf("1\n");
}
else
{
int Lp=(Pd-(S.size()));
printf("%d\n",int((S.size())/Lp));
}
}
}
第一次出现的位置
很明显维护每一个节点\(R\)集合的最小值
所有出现的位置
维护对应节点的\(R\)即可,大概要用启发式合并?
其实直接遍历子树就行了
最短的没有出现的字符串
一个很zz的问题,就是一层一层的找到第一个连边没有覆盖到所有字符集的节点
两个字符串的最长公共子串
假设有两个串为\(S,T\)
我们对\(S\)建\(SAM\),考虑\(T\)的前\(i\)为的匹配的最长后缀,答案明显显然为最大值
然后考虑优化这个过程,把\(T\)前\(i\)的字符串放进\(SAM\)匹配最长后缀,设当前匹配到\(p\),长度为\(l\)
添加\(T_{i+1}\),如果\(p\)有连边就直接走,否则跳\(Link\)链找第一个满足的
多个字符串的最长公共子串
考虑沿用上道题的思路
同样这样匹配,我们计算在当前\(S_i\)的匹配中节点能匹配的最长长度
而每个节点在多个匹配中的答案显然是每次匹配的最小值
至于最长长度,从\(Link\)树上\(dfs\)即可
这是推特上一个卖屁股的福利姬,
她用卖屁股攒下来的钱,买的公寓烂尾了……
315要啥动静?315报道过一次开发商烂尾吗?#include<bits/stdc++.h>
const int MAXN=1e5+5;
using namespace std;
struct SAM_node{
int Len;
int Next[26];
int Link;
};
struct SAM{
SAM_node Tree[MAXN*2];
int cnt_node;
vector<int>g[MAXN*2];
int Last;
int mx[MAXN*2];
int mi[MAXN*2];
int Flag_Isbuild;
void Build()
{
cnt_node=0;
Last=0;
Tree[0].Link=-1;
Tree[0].Len=0;
return;
}
void Insert(char s)
{
if(!Flag_Isbuild)
{
Flag_Isbuild=1;
Build();
}
int p=Last;
int cur=++cnt_node;
p=Tree[Last].Link;
Tree[Last].Next[s-'a']=cur;
Tree[cur].Len=Tree[Last].Len+1;
while((p!=-1)&&(!Tree[p].Next[s-'a']))
{
Tree[p].Next[s-'a']=cur;
p=Tree[p].Link;
}
if(p==-1)
{
Tree[cur].Link=0;
}
else
{
int q=Tree[p].Next[s-'a'];
if(Tree[q].Len==Tree[p].Len+1)
{
Tree[cur].Link=q;
}
else
{
int clone=++cnt_node;
Tree[clone]=Tree[q];
Tree[clone].Len=Tree[p].Len+1;
Tree[q].Link=clone;
Tree[cur].Link=clone;
while((p!=-1)&&(Tree[p].Next[s-'a']==q))
{
Tree[p].Next[s-'a']=clone;
p=Tree[p].Link;
}
}
}
Last=cur;
}
void LinkB()
{
for(int i=0;i<=cnt_node;i++)
{
if(Tree[i].Link==-1)
{
continue;
}
g[Tree[i].Link].push_back(i);
}
}
void dfs(int x)
{
for(int i=0;i<g[x].size();i++)
{
int v=g[x][i];
dfs(v);
mx[x]=max(mx[x],mx[v]);
}
mx[x]=min(mx[x],Tree[x].Len);
mi[x]=min(mi[x],mx[x]);
}
}sam;
bool cmp(string x,string y)
{
return x.size()<y.size();
}
string S[15];
int main()
{
//freopen("date.in","r",stdin);
// freopen("rnm.out","w",stdout);
int Cnt;
scanf("%d",&Cnt);
for(int i=1;i<=Cnt;i++)
{
cin>>S[i];
}
sort(S+1,S+1+Cnt,cmp);
for(int i=0;i<S[1].size();i++)
{
sam.Insert(S[1][i]);
}
sam.LinkB();
for(int i=0;i<=sam.cnt_node;i++)
{
sam.mi[i]=0x3f3f3f3f;
}
for(int C=2;C<=Cnt;C++)
{
int p=0;
int l=0;
int Ans=0;
for(int i=0;i<=sam.cnt_node;i++)
{
sam.mx[i]=0;
}
for(int i=0;i<S[C].size();i++)
{
if(sam.Tree[p].Next[S[C][i]-'a'])
{
p=sam.Tree[p].Next[S[C][i]-'a'];
l++;
}
else
{
while((p!=-1)&&(!sam.Tree[p].Next[S[C][i]-'a']))
{
p=sam.Tree[p].Link;
l=sam.Tree[p].Len;
}
if(p==-1)
{
p=0;
l=0;
}
else
{
p=sam.Tree[p].Next[S[C][i]-'a'];
l++;
}
}
sam.mx[p]=max(l,sam.mx[p]);
}
sam.dfs(0);
}
int Ans=0;
for(int i=sam.cnt_node;i>=1;i--)
{
Ans=max(Ans,sam.mi[i]);
}
printf("%d\n",Ans);
}
一些例题
甲苯先生和大中锋的字符串
不用多说,字串出现次数明显可以维护,剩下的就是简单区间加
CF802I Fake News (hard)
和上道题差不多的
SP8093 JZPGYZ - Sevenk Love Oimaster
这是个广义后缀自动机
在这里先明确一下\(clone\)的含义
在做后缀自动机的时候我们在\(len_p+1=len_q\)时直接将\(Link_{cur}=q\),相当于\(cur\)的\(Pos\)是可以直接添加进\(R\)中的
而\(len_p+1<len_q\),说明有另一条路径可以抵达\(q\)且更长,而\(cur\)对应的\(Pos\)可以作为另一个右端点从\(q\)的字符串中分出一部分,所以要建立一个\(clone\),同时\(Pos\in R_{clone},\notin R_q\)
明确之后,对多串建立一个\(SAM\),一个串插完后直接令\(Last=0\),但要注意现在的\(Last\)可能有\(ch\)的连边
回到这道题,建完后直接启发式合并\(set\)即可
#include<bits/stdc++.h>
const int MAXN=1e5+5;
using namespace std;
struct SAM_node{
int Len;
int Next[26];
int Link;
set<int>Num;
};
int Ans[MAXN*2];
struct SAM{
SAM_node Tree[MAXN*2];
int cnt_node;
vector<int>g[MAXN*2];
int Last;
int mx[MAXN*2];
int mi[MAXN*2];
int Flag_Isbuild;
vector<int>query[MAXN*2];
int New()
{
++cnt_node;
Tree[cnt_node].Link=0;
for(int i=0;i<26;i++)
{
Tree[cnt_node].Next[i]=0;
}
Tree[cnt_node].Len=0;
return cnt_node;
}
void Build()
{
cnt_node=0;
Last=0;
for(int i=0;i<26;i++)
{
Tree[cnt_node].Next[i]=0;
}
Tree[0].Link=-1;
Tree[0].Len=0;
return;
}
void Insert(char s,int Pos)
{
if(!Flag_Isbuild)
{
Flag_Isbuild=1;
Build();
}
if(Tree[Last].Next[s-'a'])
{
int p=Last;
int q=Tree[p].Next[s-'a'];
if(Tree[q].Len==Tree[p].Len+1)
{
Tree[q].Num.insert(Pos);
Last=q;
}
else
{
int clone=++cnt_node;
for(int i=0;i<26;i++)
{
Tree[clone].Next[i]=Tree[q].Next[i];
}
Tree[clone].Num.clear();
Tree[clone].Len=Tree[p].Len+1;
Tree[clone].Link=Tree[q].Link;
Tree[q].Link=clone;
while((p!=-1)&&(Tree[p].Next[s-'a']==q))
{
Tree[p].Next[s-'a']=clone;
p=Tree[p].Link;
}
Tree[clone].Num.insert(Pos);
Last=clone;
}
return;
}
int p=Last;
int cur=++cnt_node;
p=Tree[Last].Link;
Tree[Last].Next[s-'a']=cur;
Tree[cur].Len=Tree[Last].Len+1;
while((p!=-1)&&(!Tree[p].Next[s-'a']))
{
Tree[p].Next[s-'a']=cur;
p=Tree[p].Link;
}
if(p==-1)
{
Tree[cur].Link=0;
}
else
{
int q=Tree[p].Next[s-'a'];
if(Tree[q].Len==Tree[p].Len+1)
{
Tree[cur].Link=q;
}
else
{
int clone=++cnt_node;
for(int i=0;i<26;i++)
{
Tree[clone].Next[i]=Tree[q].Next[i];
}
Tree[clone].Link=Tree[q].Link;
Tree[clone].Num.clear();
Tree[clone].Len=Tree[p].Len+1;
Tree[q].Link=clone;
Tree[cur].Link=clone;
while((p!=-1)&&(Tree[p].Next[s-'a']==q))
{
Tree[p].Next[s-'a']=clone;
p=Tree[p].Link;
}
}
}
Tree[cur].Num.insert(Pos);
Last=cur;
}
void LinkB()
{
for(int i=0;i<=cnt_node;i++)
{
g[i].clear();
}
for(int i=0;i<=cnt_node;i++)
{
if(Tree[i].Link==-1)
{
continue;
}
g[Tree[i].Link].push_back(i);
}
}
void dfs(int x)
{
for(int i=0;i<g[x].size();i++)
{
int v=g[x][i];
dfs(v);
if(Tree[v].Num.size()>Tree[x].Num.size())
{
swap(Tree[v].Num,Tree[x].Num);
}
for(set<int>::iterator it=Tree[v].Num.begin();it!=Tree[v].Num.end();it++)
{
Tree[x].Num.insert((*it));
}
}
for(int i=0;i<query[x].size();i++)
{
Ans[query[x][i]]=Tree[x].Num.size();
}
}
void Query(string S,int Pos)
{
int p=0;
for(int i=0;i<S.size();i++)
{
if(Tree[p].Next[S[i]-'a'])
{
p=Tree[p].Next[S[i]-'a'];
}
else{
return;
}
}
query[p].push_back(Pos);
}
}sam;
int T;
string S;
int n,m;
int main()
{
///freopen("date.in","r",stdin);
scanf("%d %d",&n,&m);
for(int i=1;i<=n;i++)
{
cin>>S;
for(int j=0;j<S.size();j++)
{
sam.Insert(S[j],i);
}
sam.Last=0;
}
for(int i=1;i<=m;i++)
{
cin>>S;
sam.Query(S,i);
}
sam.LinkB();
sam.dfs(0);
for(int i=1;i<=m;i++)
{
printf("%d\n",Ans[i]);
}
}
[BJOI2020] 封印
设\(Lp_i\)为\(S\)以\(i\)结尾最长匹配长度
很明显答案为\(max(min(Lp_i,i-L+1))\),注意到\(Lp_i-i\)单减
[ZJOI2015]诸神眷顾的幻想乡
这里说说广义后缀自动机的另一种建法
我们认为\(NYH\)是神,所以就知道怎么建了
考虑把所有的字符串先插入\(Tire\)再\(Bfs\)建后缀自动机
回到这到题,注意到叶子数很少,如果我们以每个叶子为根建\(Tire\)后插入后缀自动机,很明显是可以覆盖到所有子串的
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
struct SAM_node{
int Len;
int Next[10];
int Link;
};
struct SAM{
SAM_node Tree[MAXN*2];
int Last;
int cnt_node;
void Build()
{
cnt_node=0;
Tree[0].Len=0;
Tree[0].Link=-1;
return;
}
int Insert(int p,int ch)
{
int cur=Tree[p].Next[ch];
Tree[cur].Len=Tree[p].Len+1;
p=Tree[p].Link;
while((p!=-1)&&(!Tree[p].Next[ch]))
{
Tree[p].Next[ch]=cur;
p=Tree[p].Link;
}
if(p==-1)
{
Tree[cur].Link=0;
return cur;
}
else
{
int q=Tree[p].Next[ch];
if(Tree[p].Len+1==Tree[q].Len)
{
Tree[cur].Link=q;
return cur;
}
else
{
int clone=++cnt_node;
for(int i=0;i<10;i++)
{
if(Tree[Tree[q].Next[i]].Len)
{
Tree[clone].Next[i]=Tree[q].Next[i];
}
}
Tree[clone].Len=Tree[p].Len+1;
Tree[clone].Link=Tree[q].Link;
Tree[q].Link=clone;
Tree[cur].Link=clone;
while((p!=-1)&&(Tree[p].Next[ch]==q))
{
Tree[p].Next[ch]=clone;
p=Tree[p].Link;
}
return cur;
}
}
}
void Bfs()
{
queue<pair<int,int> >q;
for(int i=0;i<10;i++)
{
if(Tree[0].Next[i])
{
q.push(make_pair(0,i));
}
}
while(q.size())
{
pair<int,int>temp=q.front();
q.pop();
int Cur=Insert(temp.first,temp.second);
for(int i=0;i<10;i++)
{
if(Tree[Cur].Next[i])
{
q.push(make_pair(Cur,i));
}
}
}
return;
}
}sam;
int n,C;
int rd[MAXN];
int Clor[MAXN];
vector<int>g[MAXN];
int Vis[MAXN];
int main()
{
freopen("date.in","r",stdin);
freopen("date.out","w",stdout);
sam.Build();
scanf("%d %d",&n,&C);
for(int i=1;i<=n;i++)
{
scanf("%d",&Clor[i]);
}
for(int i=1;i<n;i++)
{
int x,y;
scanf("%d %d",&x,&y);
g[x].push_back(y);
g[y].push_back(x);
rd[x]++;
rd[y]++;
}
for(int x=1;x<=n;x++)
{
if(rd[x]==1)
{
queue<pair<int,int> >q;
memset(Vis,0,sizeof(Vis));
q.push(make_pair(x,0));
while(q.size())
{
pair<int,int>temp=q.front();
q.pop();
Vis[temp.first]=1;
int Popp=temp.second;
int Too=Clor[temp.first];
if(!sam.Tree[Popp].Next[Too])
{
sam.Tree[Popp].Next[Too]=++sam.cnt_node;
}
int Cur=sam.Tree[Popp].Next[Too];
for(int i=0;i<g[temp.first].size();i++)
{
int v=g[temp.first][i];
if(Vis[v])
{
continue;
}
q.push(make_pair(v,Cur));
}
}
}
}
sam.Bfs();
long long Ans=0;
for(int i=1;i<=sam.cnt_node;i++)
{
Ans+=(sam.Tree[i].Len-sam.Tree[sam.Tree[i].Link].Len);
}
printf("%lld\n",Ans);
}
「雅礼集训 2017 Day7」事情的相似度
首先不难想到如果确定\(l\le i<j\le r\),我们可以直接在\(SAM\)的\(Link\)树上找\(i,j\)对应节点的\(LCA\)
照这个思路,我们可以考虑每个节点对一个询问的影响,考虑用\(bitset\)记录每个节点能覆盖的询问
注意这里我们要按\(Len\)从大到小合并,一个询问如果被在他的子树即本身覆盖两次及以上,那这个节点就是答案,为了保证一个询问只会访问到一次,我们要记录是否被访问
还有就是这个空间好像是\(1e9\),我们可以把\(m\)分成\(4\)部分计算
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
const int Lit=(MAXN/4);
struct SAM_node{
int link,Next[2],Len;
}Tree[MAXN*2];
int cnt_node;
int Last=0;
int n,m;
string s;
vector<int>g[MAXN*2];
int Rec[MAXN*2];
void Build()
{
cnt_node=0;
Last=0;
Tree[0].link=-1;
Tree[0].Len=0;
for(int i=0;i<2;i++)
{
Tree[0].Next[i]=0;
}
return;
}
int Insert(int p,int sh)
{
int cur=++cnt_node;
Tree[cur].Len=Tree[p].Len+1;
while((p!=-1)&&(!Tree[p].Next[sh]))
{
Tree[p].Next[sh]=cur;
p=Tree[p].link;
}
if(p==-1)
{
Tree[cur].link=0;
}
else
{
int q=Tree[p].Next[sh];
if(Tree[q].Len==Tree[p].Len+1)
{
Tree[cur].link=q;
}
else
{
int clone=++cnt_node;
Tree[clone].link=Tree[q].link;
Tree[q].link=clone;
Tree[cur].link=clone;
for(int i=0;i<2;i++)
{
Tree[clone].Next[i]=Tree[q].Next[i];
}
Tree[clone].Len=Tree[p].Len+1;
while((p!=-1)&&(Tree[p].Next[sh]==q))
{
Tree[p].Next[sh]=clone;
p=Tree[p].link;
}
}
}
return cur;
}
int l,r;
bitset<Lit>S[MAXN*2];
bitset<Lit>Used,New;
int Ans[MAXN*2];
int Rsort[MAXN*2];
struct Query{
int l,r;
}query[MAXN];
void solve()
{
if(!m)
{
return;
}
int Cnt=0;
while(m)
{
m--;
scanf("%d %d",&l,&r);
query[Cnt].l=l;
query[Cnt].r=r;
Cnt++;
if(Cnt==Lit)
{
break;
}
}
for(int i=0;i<=cnt_node;i++)
{
S[i].reset();
}
for(int i=0;i<Cnt;i++)
{
Used.set(i);
S[Rec[query[i].l]].set(i);
S[Rec[query[i].r+1]].set(i);
}
for(int i=2;i<=n;i++)
{
S[Rec[i]]^=S[Rec[i-1]];
}
for(int ps=0;ps<=cnt_node;ps++)
{
int x=Rsort[ps];
for(int i=0;i<g[x].size();i++)
{
int v=g[x][i];
New=S[x]&S[v]&Used;
S[x]|=S[v];
for(int j=New._Find_first();j!=New.size();j=New._Find_next(j))
{
Ans[j]=Tree[x].Len;
Used.reset(j);
}
}
}
for(int i=0;i<Cnt;i++)
{
printf("%d\n",Ans[i]);
}
}
int cmp(int x,int y)
{
return Tree[x].Len>Tree[y].Len;
}
int main()
{
freopen("date.in","r",stdin);
// freopen("date.out","w",stdout);
Build();
scanf("%d %d",&n,&m);
cin>>s;
for(int i=0;i<s.size();i++)
{
Last=Insert(Last,s[i]-'0');
Rec[i+1]=Last;
}
for(int i=0;i<=cnt_node;i++)
{
if(Tree[i].link==-1)
{
continue;
}
g[Tree[i].link].push_back(i);
Rsort[i]=i;
}
sort(Rsort,Rsort+cnt_node+1,cmp);
solve();
solve();
solve();
solve();
}