【洛谷6152】[集训队作业2018] 后缀树节点数(SAM+LCT)
- 给定一个长度为\(n\)的字符串\(S\),\(m\)次询问\(S[L:R]\)的后缀树节点数。
- \(n\le10^5,m\le3\times10^5\)
后缀自动机节点分类
由于后缀树相当于是反串的后缀自动机,所以我们先将原串翻转,再把每个询问区间\([L,R]\)修改为\([n-R+1,n-L+1]\),就变成了每次询问一个子串的后缀自动机节点数。
而子串后缀自动机上的节点共有两类:子串的前缀节点;原串后缀自动机上至少两个子节点子树中在区间内有串的节点。
我们先分别独立计算满足某一条件的点数,然后减去同时满足这两个条件的点数即可。
其中子串的前缀节点个数显然是\(R-L+1\),关键是另外两问。
分裂节点个数
把询问按照右端点排序,那么就可以逐渐扩展右端点,维护每个左端点的答案。
于是考虑右端点扩展一位更新的贡献,找到这个前缀在\(parent\)树中对应的节点,对于它到根路径上所有具有其他儿子的节点\(x\),假设这个节点对应串长度为\(len_x\),其他儿子中最近一次访问的右端点是\(P_x\),那么这个节点就可以对小于等于\(lst_x=P_x-len_x\)的所有左端点产生贡献(注意删去这个节点原先的贡献)。
这个“其他儿子中最近一次访问的右端点”显得非常奇怪,容易发现如果最近一次访问的右端点和当前右端点在同一儿子中,实际上不会对答案造成影响。
所以我们可以转化一下,对每个节点维护最近一次访问的右端点\(P_x\),而每次询问对象只限于最近一次访问来自其他儿子的节点。
然后发现这正是\(LCT\)的\(Access\)操作:\(Access\)过程中进行虚实链切换的节点就是最近一次访问来自其他儿子的节点,维护最近一次访问的右端点相当于给\(Access\)得到的这条链染色。
因此用\(LCT\)即可实现这一过程,而贡献的维护可以利用树状数组。
前缀分裂节点个数
首先我们发现,如果前缀\([L,i]\)会分裂,那么前缀\([L,i-1]\)就更会分裂。因为\([L,i]\)会分裂说明存在\(c_1+S[L,i]\)和\(c_2+S[L,i]\)两种子串,则也就存在\(c_1+S[L,i-1]\)和\(c_2+S[L,i-1]\)两种子串。
因此可以二分能够分裂的最长前缀,那么长度小于等于它的前缀都在先前的答案中被计算了两遍,需要减去它们的个数。
然后就是如何检验的问题了。我们先通过树上倍增找到这个子串在\(parent\)树上对应的节点\(x\),如果它没达到这个节点的最大长度肯定不会分裂(即它在原后缀自动机中都不是一个分裂节点,那么在这个子区间后缀自动机中就更不可能了),否则只需利用前面计算出的\(lst_x\)判断一下\(L\)是否小于等于\(lst_x\)即可。
代码:\(O(mlog^2n)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Rg register
#define RI Rg int
#define Cn const
#define CI Cn int&
#define I inline
#define W while
#define N 100000
#define M 300000
#define LN 17
using namespace std;
int n,m,a[N+5],id[N+5],lst[2*N+5],ans[M+5];vector<pair<int,int> > q[N+5];
namespace FastIO
{
#define FS 100000
#define tc() (FA==FB&&(FB=(FA=FI)+fread(FI,1,FS,stdin),FA==FB)?EOF:*FA++)
#define pc(c) (FC==FE&&(clear(),0),*FC++=c)
int OT;char oc,FI[FS],FO[FS],OS[FS],*FA=FI,*FB=FI,*FC=FO,*FE=FO+FS;
I void clear() {fwrite(FO,1,FC-FO,stdout),FC=FO;}
Tp I void read(Ty& x) {x=0;W(!isdigit(oc=tc()));W(x=(x<<3)+(x<<1)+(oc&15),isdigit(oc=tc()));}
Tp I void writeln(Ty x) {W(OS[++OT]=x%10+48,x/=10);W(OT) pc(OS[OT--]);pc('\n');}
}using namespace FastIO;
struct BIT {int a[N+5];I void U(RI x,CI v) {W(x) a[x]+=v,x-=x&-x;}I int Q(RI x,RI t=0) {W(x<=n) t+=a[x],x+=x&-x;return t;}}T;
namespace SAM//后缀自动机
{
int Nt=1,lst=1;struct node {int L,F[LN+1];map<int,int> S;}O[2*N+5];I int Ins(CI x)//添加字符,返回节点编号
{
RI p=lst,o=lst=++Nt;O[o].L=O[p].L+1;W(p&&!O[p].S[x]) O[p].S[x]=o,p=O[p].F[0];if(!p) return O[o].F[0]=1,o;
RI q=O[p].S[x];if(O[q].L==O[p].L+1) return O[o].F[0]=q,o;RI k=++Nt;
(O[k]=O[q]).L=O[p].L+1,O[q].F[0]=O[o].F[0]=k;W(p&&O[p].S[x]==q) O[p].S[x]=k,p=O[p].F[0];return o;
}
int c[N+5],q[2*N+5];I void Work();
I int Get(CI l,CI r) {RI x=id[r];for(RI i=LN;~i;--i) O[O[x].F[i]].L>=r-l+1&&(x=O[x].F[i]);return x;}//树上倍增求子串对应节点
}
class LinkCutTree
{
private:
#define IR(x) (O[O[x].F].S[0]^x&&O[O[x].F].S[1]^x)
#define Wh(x) (O[O[x].F].S[1]==x)
#define Co(x,y,d) (O[O[x].F=y].S[d]=x)
#define PD(x) (O[x].G&&(T(O[x].S[0],O[x].G),T(O[x].S[1],O[x].G),O[x].G=0))
#define T(x,v) (O[x].P=O[x].G=v)
struct node {int P,F,G,S[2];}O[2*N+5];
I void Ro(RI x) {RI f=O[x].F,p=O[f].F,d=Wh(x);!IR(f)&&(O[p].S[Wh(f)]=x),O[x].F=p,Co(O[x].S[d^1],f,d),Co(f,x,d^1);}
int St[2*N+5];I void S(RI x)
{
RI f=x,T=0;W(St[++T]=f,!IR(f)) f=O[f].F;W(T) PD(St[T]),--T;W(!IR(x)) f=O[x].F,!IR(f)&&(Ro(Wh(f)^Wh(x)?x:f),0),Ro(x);
}
public:
I void Ac(RI x,CI i)//Access
{
RI y;for(y=0;x;x=O[y=x].F) S(x),x^1&&O[x].P&&(T.U(lst[x],-1),T.U(lst[x]=O[x].P-SAM::O[x].L,1),0),O[x].S[1]=y;T(y,i);//更新贡献;给链染色
}
I void Link(CI x,CI y) {O[y].F=x;}
}LCT;
I void SAM::Work()//建出后缀自动机后的一些处理
{
RI i;for(i=1;i<=Nt;++i) ++c[O[i].L];for(i=1;i<=n;++i) c[i]+=c[i-1];for(i=1;i<=Nt;++i) q[c[O[i].L]--]=i;//基排
RI j,x;for(i=2;i<=Nt;++i) for(x=q[i],LCT.Link(O[x].F[0],x),j=1;j<=LN;++j) O[x].F[j]=O[O[x].F[j-1]].F[j-1];//LCT上连边,预处理倍增数组
}
I int G(CI L,RI r)//求出前缀分裂节点个数
{
RI l=L-1,mid,x;W(l^r) x=SAM::Get(L,mid=l+r+1>>1),SAM::O[x].L==mid-L+1&&L<=lst[x]?l=mid:r=mid-1;return l-L+1;//二分答案找到对应节点检验
}
int main()
{
RI i,x,y;for(read(n),read(m),i=1;i<=n;++i) read(a[i]);for(i=1;i<=n;++i) id[i]=SAM::Ins(a[n-i+1]);SAM::Work();//建后缀自动机
for(i=1;i<=m;++i) read(x),read(y),q[n-x+1].push_back(make_pair(i,n-y+1));//将询问离线,按右端点排序
vector<pair<int,int> >::iterator it;for(i=1;i<=n;++i) for(LCT.Ac(id[i],i),//扩展右端点更新贡献
it=q[i].begin();it!=q[i].end();++it) ans[it->first]=(i-it->second+1)+T.Q(it->second)-G(it->second,i);//前缀节点个数+分裂节点个数-前缀分裂节点个数
for(i=1;i<=m;++i) writeln(ans[i]);return clear(),0;
}