P13665 「TPOI-5D」「僕は…」可恶的串串题
题意
给定 \(n\) 个总长不超过 \(M\) 的小写字母字符串,然后问第 \(l\) 到第 \(r\) 个字符串在第 \(L\) 到第 \(R\) 个字符串中出现了多少次。
解法
这种东西一看就是数据结构加 ACAM 或 SAM 或后缀数组,后面两个我不会,所以不妨考虑 ACAM。
首先考虑弱化版 \(l=r\) 怎么做。考虑建出 AC 自动机,注意到原查询等价于查 \(l\) 对 \(1\) 到 \(R\) 的贡献减 \(1\) 到 \(L\) 的贡献,这启示我们可以进行扫描线。
问题变成了查询某一个串对 \(1\) 到 \(R\) 的贡献,不难发现在 fail 树上一个节点的子树内肯定都含有这个这个节点在字典树上对应的字符串,然后查询就变成了子树和。
然后在扫描线加入时将这个字符串所有前缀状态在字典树上对应的值加 \(1\),因为字符串总长一定,所以这个过程可以暴力加,子树和可以拍成 dfs 序,你使用树状数组或别的东西可以做到一只 \(\log\)。
好,我们考虑这个题怎么做,发现这种东西可能还是要带个根号,所以考虑分块。对于每一个块,我们记 \(pre_{i,j}\) 表示第 \(i\) 个块对前 \(j\) 个字符串的贡献。
这个东西我们可以先把一个块内的字符串的对应节点加 \(1\),然后在 fail 树上做一个前缀和,即树上的点是它所有祖先的之和,对每个字符串的贡献就是字符串前缀所有节点的前缀和的和,你注意到其实就是倒着做上面扫描线的部分。这样显然对于一个块内是 \(\mathcal O (n)\) 的,可以接受。
然后对于散块,你可以拆成 \(\mathcal O (n \sqrt n)\) 次削弱版的询问,于是你得到了一个 \(\mathcal O (n \sqrt{n \log n})\) 的做法,显然过不去。由于是 \(O(n)\) 次修改与 \(\mathcal O (n \sqrt n)\) 次询问,使用 \(\mathcal O (1) - \mathcal O (\sqrt n)\) 分块可以简单转化成 \(\mathcal O (n\sqrt n)\)。
好,这里的内存使得你的整块和散块双双爆炸,我们优化一下。对于整块,离线下来以后逐块处理,简单地优化到 \(\mathcal O(n)\),对于散块,我们扫描线时右端点是一样的,只是查询的东西不一样,我们可以把散块区间记进去,空间复杂度就变成 \(\mathcal O(n)\) 了。
这题相当卡常,如果你过不去请尝试对于多个块处理一次。
代码
可以给一份卡常前的,可读性高一点。
#include<bits/stdc++.h>
using namespace std;
const int N=200005,S=300;
char a[N];
int id[200005],n,m,cnt,cd[200005][26],tot=1,ff[200005],bel[200005],L[200001],R[200001],fl[N];
int l1[200005],r1[200005],l2[200005],r2[200005],sum[2000005],sz[N+5],ds[N+5];
long long pre[N+5],ans[200005],f[200005];
vector<int>e[N+5];
inline void ins(int asd){
int x=1,len=strlen(a);
for(int i=0;i<len;i++){
if(!cd[x][a[i]-'a']){ //cd 表示字典树
cd[x][a[i]-'a']=++tot;ff[tot]=x;
}
x=cd[x][a[i]-'a'];
}
id[asd]=x;
} //插入
void add(int x){
for(int i=x;i<=R[bel[x]];i++)pre[i]++;
for(int i=bel[x];i<=bel[tot];i++)f[i]++;
}
int query(int l,int r){
int res=0;
if(bel[l]==bel[r]){
if(l!=L[bel[l]])return pre[r]-pre[l-1];
else return pre[r];
}
else{
if(l!=L[bel[l]])res+=pre[R[bel[l]]]-pre[l-1];
else res+=pre[R[bel[l]]];
res+=pre[r];
res+=f[bel[r]-1]-f[bel[l]];
}
return res;
} //这是 O(sqrt n) - O(1) 分块
void fail(){
fl[1]=0;
queue<int>q;q.push(1);
for(int i=0;i<=25;i++)cd[0][i]=1;
while(!q.empty()){
int u=q.front();q.pop();
for(int i=0;i<=25;i++){
if(!cd[u][i])cd[u][i]=cd[fl[u]][i];
else{
fl[cd[u][i]]=cd[fl[u]][i]; //fl 就是 fail 指针
e[cd[fl[u]][i]].push_back(cd[u][i]);
q.push(cd[u][i]);
}
}
}
}
void bfs(){
queue<int>q;
q.push(1);
while(!q.empty()){
int u=q.front();
q.pop();
for(int v:e[u]){
sum[v]+=sum[u];
q.push(v); //这就是前缀和的过程,当然也可以dfs
}
}
}
struct node{
int ll,rr,r,id,type;
}que[4*N];
bool cmp(node x,node y){
return x.r<y.r;
}
int iidd=0;
void dfs(int u){
sz[u]=1;
ds[u]=++iidd; //ds 是 dfs 序
for(int v:e[u]){
dfs(v);
sz[u]+=sz[v];
}
}
signed main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a;
ins(i);
}
fail();
for(int i=1;i<=n;i++)bel[i]=(i-1)/S+1;
for(int i=1;i<=n;i++)R[bel[i]]=i;
for(int i=n;i>=1;i--)L[bel[i]]=i;
for(int i=1;i<=m;i++){
cin>>l1[i]>>r1[i]>>l2[i]>>r2[i];
}
for(int i=1;i<=bel[n];i++){ //这里是整块的逐块处理
for(int j=L[i];j<=R[i];j++)sum[id[j]]++;
bfs();
for(int j=1;j<=n;j++){
int x=id[j];
pre[j]=0;
while(x){
pre[j]+=sum[x];x=ff[x];
}
pre[j]+=pre[j-1];
}
for(int j=1;j<=m;j++){
if(l1[j]<L[i]&&R[i]<r1[j]){
ans[j]+=pre[r2[j]]-pre[l2[j]-1];
}
}
for(int j=1;j<=tot;j++)sum[j]=0;
}
for(int i=1;i<=m;i++){
if(bel[l1[i]]==bel[r1[i]]){
que[++cnt]={l1[i],r1[i],r2[i],i,1};
que[++cnt]={l1[i],r1[i],l2[i]-1,i,-1};
}else{
que[++cnt]={l1[i],R[bel[l1[i]]],r2[i],i,1};
que[++cnt]={l1[i],R[bel[l1[i]]],l2[i]-1,i,-1};
que[++cnt]={L[bel[r1[i]]],r1[i],r2[i],i,1};
que[++cnt]={L[bel[r1[i]]],r1[i],l2[i]-1,i,-1}; //此处为拆询问
}
}
for(int i=1;i<=tot;i++)bel[i]=(i-1)/S+1; //可以换个块长,卡常轻松一点
for(int i=1;i<=tot;i++)R[bel[i]]=i;
for(int i=tot;i>=1;i--)L[bel[i]]=i;
dfs(1);
memset(pre,0,sizeof(pre));
sort(que+1,que+1+cnt,cmp);
int idx=1;
while(que[idx].r==0)idx++;
for(int i=1;i<=n;i++){
int x=id[i];
while(x!=0){
add(ds[x]);x=ff[x];
}
while(i==que[idx].r){
for(int j=que[idx].ll;j<=que[idx].rr;j++){
ans[que[idx].id]+=que[idx].type*query(ds[id[j]],ds[id[j]]+sz[id[j]]-1);
}
idx++;
}
}
for(int i=1;i<=m;i++){
cout<<ans[i]<<endl;
}
}

浙公网安备 33010602011771号