题目链接:点击查看
题目大意:给出一个长度为 n 的字符串,再给出 m 次询问,每次询问需要回答区间 [ l , r ] 内有多少个本质不同的字符串
题目分析:首先简化模型,回顾一下如何求解 “区间内有多少个不同的数” :SPOJ - DQUERY
求解上面那道题目的方法可谓是五花八门,我们采用其中一种离线的方式:先将所有的询问储存下来,按照右端点 r 排序后,每次对于重复出现的数字,用线段树维护其最后一次出现的位置,那么答案就是 [ l , r ] 中元素的个数了
对于这个题目而言,将每个本质不同的字符串视为一个连续的区间 [ l , r ],我们只需要维护左端点最后一次出现的位置即可
因为需要知道每个子串最后一次出现的位置,所以我们选择对字符串构造后缀自动机,对于某个右端点 r 来说,对应到 SAM 上,从最长的那个前缀 [ 1 , r ] 对应的节点开始,沿着 parent 树到根节点的这条路径上,都是以 r 为右端点的子串,设 pre[ i ] 为节点 i 上一次出现时的右端点位置,我们只需要暴跳 father,每次将之前出现过的位置,在线段树上区间更新成 -1 (因为对于每个节点来说,代表的都是一段连续的后缀,所以可以用到线段树的区间更新),最后再将 [ 1 , r ] , [ 2 , r ] ... [ r , r ] 这 r 个后缀所代表的子串的左端点,即 [ 1 , r ] 在线段树上 +1 就好了
不过问题是,这样暴跳 father 的时间复杂度是不正确的,直接这样实现在本题中只能得到 80 分,有一个测试点会超时
考虑优化,因为每次选择一条链,自下而上去更新,其本质就是:
- 令这条链每个节点相应的位置在线段树上区间更新
- 令每个节点都被端点 r 覆盖(也就是将 pre 都标记为 r)
这个过程其实就是 LCT 中的 access 函数的操作,这样每次更新至多需要更新 logn 个 splay,套上线段树就是 log^2n,均摊下来就是 nlog^2n 了
因为在 SAM 自底向上的一条路径上,其中任意连续的一段,所能表示的后缀也是连续的,所以在 LCT 的 splay 上只需要维护一下相应重链上最短的子串长度就好了
因为在本题中用不到 LCT 的 link、cut 等操作,所以将相应的一些基本函数都删掉了,只保留了与 access 函数有关的一些基本函数
总时间复杂度是 O( nlog^2n + mlogn )
代码:
//#pragma GCC optimize(2)
//#pragma GCC optimize("Ofast","inline","-ffast-math")
//#pragma GCC target("avx,sse2,sse3,sse4,mmx")
#include<iostream>
#include<cstdio>
#include<string>
#include<ctime>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<stack>
#include<climits>
#include<queue>
#include<map>
#include<set>
#include<sstream>
#include<cassert>
#include<bitset>
using namespace std;
typedef long long LL;
typedef unsigned long long ull;
const int inf=0x3f3f3f3f;
const int N=1e6+100;
char s[N];
vector<pair<int,int>>q[N];
LL ans[N];
namespace Seg
{
struct Node
{
int l,r,len;
LL sum,lazy;
}tree[N<<2];
void pushup(int k)
{
tree[k].sum=tree[k<<1].sum+tree[k<<1|1].sum;
}
void pushdown(int k)
{
if(tree[k].lazy)
{
LL lz=tree[k].lazy;
tree[k].lazy=0;
tree[k<<1].sum+=tree[k<<1].len*lz;
tree[k<<1|1].sum+=tree[k<<1|1].len*lz;
tree[k<<1].lazy+=lz;
tree[k<<1|1].lazy+=lz;
}
}
void build(int k,int l,int r)
{
tree[k].l=l;
tree[k].r=r;
tree[k].len=r-l+1;
tree[k].sum=tree[k].lazy=0;
if(l==r)
return;
int mid=l+r>>1;
build(k<<1,l,mid);
build(k<<1|1,mid+1,r);
}
void update(int k,int l,int r,LL val)
{
if(tree[k].l>r||tree[k].r<l)
return;
if(tree[k].r<=r&&tree[k].l>=l)
{
tree[k].sum+=tree[k].len*val;
tree[k].lazy+=val;
return;
}
pushdown(k);
update(k<<1,l,r,val);
update(k<<1|1,l,r,val);
pushup(k);
}
LL query(int k,int l,int r)
{
if(tree[k].l>r||tree[k].r<l)
return 0;
if(tree[k].l>=l&&tree[k].r<=r)
return tree[k].sum;
pushdown(k);
return query(k<<1,l,r)+query(k<<1|1,l,r);
}
}
namespace SAM
{
int tot,last,pos[N],pre[N];
struct Node
{
int ch[26];
int fa,len;
}st[N<<1];
inline int newnode()
{
tot++;
for(int i=0;i<26;i++)
st[tot].ch[i]=0;
st[tot].fa=st[tot].len=0;
return tot;
}
void add(int x)
{
int p=last,np=last=newnode();
st[np].len=st[p].len+1;
while(p&&!st[p].ch[x])st[p].ch[x]=np,p=st[p].fa;
if(!p)st[np].fa=1;
else
{
int q=st[p].ch[x];
if(st[p].len+1==st[q].len)st[np].fa=q;
else
{
int nq=newnode();
st[nq]=st[q]; st[nq].len=st[p].len+1;
st[q].fa=st[np].fa=nq;
while(p&&st[p].ch[x]==q)st[p].ch[x]=nq,p=st[p].fa;//向上把所有q都替换成nq
}
}
}
void init()
{
last=1;
tot=0;
newnode();
}
void build()
{
init();
int len=strlen(s+1);
for(int i=1;i<=len;i++)
{
add(s[i]-'a');
pos[i]=last;
}
}
}
namespace LCT
{
int fa[N],ch[N][2],pre[N],min_len[N],len[N],Stack[N],lazy[N],top;
bool son(int x)//判断点x是父节点的左儿子还是右儿子
{
return x==ch[fa[x]][1];
}
bool isroot(int x)//判断一个点是不是根节点(当前splay的根节点)
{
return ch[fa[x]][0]!=x&&ch[fa[x]][1]!=x;
}
void pushup(int x)//上传(维护信息)
{
min_len[x]=min(len[x],min(min_len[ch[x][0]],min_len[ch[x][1]]));
}
void update_lazy(int x,int lz)
{
pre[x]=lazy[x]=lz;
}
void pushdown(int x)//下传(更新反转标记用)
{
if(lazy[x])
{
update_lazy(ch[x][0],lazy[x]);
update_lazy(ch[x][1],lazy[x]);
lazy[x]=0;
}
}
void rotate(int x)//splay的旋转
{
int y=fa[x],z=fa[y],c=son(x);
ch[y][c]=ch[x][c^1];if(ch[y][c]) fa[ch[y][c]]=y;
fa[x]=z;if(!isroot(y))ch[z][son(y)]=x;
ch[x][c^1]=y;fa[y]=x;pushup(y);
}
void splay(int x)//将x转到根节点
{
Stack[top=1]=x;
for (int i=x;!isroot(i);i=fa[i])
Stack[++top]=fa[i];
while (top) pushdown(Stack[top--]);
for (int y=fa[x];!isroot(x);rotate(x),y=fa[x])
if (!isroot(y)) son(x)^son(y)?rotate(x):rotate(y);
pushup(x);
}
void access(int x,int id)//把x节点到x所在树(连通块)的根节点之间的路径全部变成重路径
{
int y;
for(y=0;x;y=x,x=fa[x])
{
splay(x),ch[x][1]=y,pushup(x);
if(pre[x])
Seg::update(1,pre[x]-SAM::st[x].len+1,pre[x]-min_len[x]+1,-1);
}
splay(y);
update_lazy(y,id);
Seg::update(1,1,id,1);
}
void build()
{
min_len[0]=inf;
for(int i=1;i<=SAM::tot;i++)
{
fa[i]=SAM::st[i].fa;
len[i]=min_len[i]=SAM::st[SAM::st[i].fa].len+1;
lazy[i]=ch[i][0]=ch[i][1]=pre[i]=0;
}
}
};
int main()
{
#ifndef ONLINE_JUDGE
// freopen("data.in.txt","r",stdin);
// freopen("data.out.txt","w",stdout);
#endif
// ios::sync_with_stdio(false);
scanf("%s",s+1);
int n=strlen(s+1);
SAM::build();
LCT::build();
Seg::build(1,1,n);
int m;
scanf("%d",&m);
for(int i=1;i<=m;i++)
{
int l,r;
scanf("%d%d",&l,&r);
q[r].emplace_back(i,l);
}
for(int i=1;i<=n;i++)
{
LCT::access(SAM::pos[i],i);
for(auto it:q[i])
ans[it.first]=Seg::query(1,it.second,i);
}
for(int i=1;i<=m;i++)
printf("%lld\n",ans[i]);
return 0;
}