传送门
题意:给定n个字符串,q次询问,每次给出正整数L,R,X
,要求输出第x个字符串在第l个字符串到第r个字符串的总匹配次数。
(字符串长度之和不大于2e5)
分析:
- 首先我们考虑对于一个给定的字符串集合,我们如何求出其中某个字符串在集合中的匹配次数?比如给定集合
cacaa,baabaa,aa
。求出aa
在集合中的总匹配次数。(易知答案是4) 。 - 我们可以把所有的串插入trie树中,每插入一个字符,就给trie树相应节点val值+1,设
aa
尾节点为endstr[k]
,插入完成后val[endstr[k]]
的值就是以aa
为前缀的字符串个数 。我们再通过fail树,求出以endstr[k]
为根的子树的点权和,二者相加就是答案了。(因为在fail树上,根节点是其子树节点的后缀,并且fail树根节点的后代节点 等价于 所有的以根节点为后缀的字符串尾节点,如下图中,aa
在fail树的后代有baa
,baabaa
,cacaa
,1+3=4)
-
搞懂如何在字符串集合中求匹配次数问题后,这题算是解出一半了。剩下的难点在于如何建主席树?
-
首先我们利用dfs序把子树求和变成了区间求和,用线段树维护区间和。
-
题目每次要求是在[l,r]字符串集合中,求x串的匹配次数,x串未必是[l,r]当中的一个。所以我们首先把1~n所有串插入AC自动机,建fail树,求出dfs序。然后每次询问我们只需要求出[l,r]字符串匹配后,
val[endstr[x]]
的子树和。也就是[1,r]的贡献减去[1,l-1]的贡献。 -
建树:枚举每个串,串i每个节点都需要在dfs序相应位置上更新一次,最后保留更新完毕的主席树版本p[i]。 p[r]-p[l-1]这棵树便是只考虑[l,r]串进行匹配后的贡献。 查询
[in[endstr[x]], out[endstr[x]]]
区间和即可。 -
其实这里建主席树难点在于,我们维护的是dfs序的区间和,但用于版本更新的每个串的每个字符。
详见注释。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define pii pair<int,int>
const int maxn = 2e5+10;
const int mx = 40;
const int mod = 1e9+5;
const ll inf = 34359738370;
const int INF = 1e9+7;
int tree[maxn][26];
int fail[maxn];
int tot=0;//ac自动机结点数
queue<int> q;
int endstr[maxn];//尾节点编号
vector<int> sid[maxn];//每个字符串 abaccd -> 010223
vector<int> ft[maxn];//建fail树
int len[maxn];//每个字符串的长度
inline void insert(char *s,int x)
{
len[x]=strlen(s);
int rt=0;
for(int i=0;i<len[x];i++)
{
int id=s[i]-'a';
sid[x].push_back(id);
if(!tree[rt][id]) tree[rt][id]=++tot;
rt=tree[rt][id];
}
endstr[x]=rt;
}
inline void getfail()
{
for(int i=0;i<26;i++)
{
if(tree[0][i])
{
q.push(tree[0][i]);
}
}
while(q.size())
{
int f=q.front();
q.pop();
ft[fail[f]].push_back(f);
for(int i=0;i<26;i++)
{
int u=tree[f][i];
if(!u)
{
tree[f][i]=tree[fail[f]][i];
}
else
{
fail[u]=tree[fail[f]][i];
q.push(u);
}
}
}
}
int in[maxn],out[maxn],dfsclock;//dfs序 子树求合转化成区间求合
inline void dfs(int rt)
{
in[rt]=++dfsclock;
for(auto i:ft[rt])
{
dfs(i);
}
out[rt]=dfsclock;
}
int segtree[maxn*50],root[maxn],cnt=0,lc[maxn*50],rc[maxn*50];//主席树
int p[maxn];//由于每个串都是多点更新 所以记录每个串更新结束时候的版本
inline void updata(int &rt,int last,int l,int r,int p)//dfs序的p位置+1
{
rt=++cnt;
segtree[rt]=segtree[last]+1;
lc[rt]=lc[last],rc[rt]=rc[last];
if(l == r) return ;
int mid=(l+r)>>1;
if(p <= mid) updata(lc[rt],lc[last],l,mid,p);
else updata(rc[rt],rc[last],mid+1,r,p);
}
inline int query(int rt1,int rt2,int l,int r,int vl,int vr)//查询区间和
{
if(vl<=l && r<=vr) return segtree[rt1]-segtree[rt2];
int mid=(l+r)>>1;
if(vr <= mid) return query(lc[rt1],lc[rt2],l,mid,vl,vr);
if(vl > mid) return query(rc[rt1],rc[rt2],mid+1,r,vl,vr);
return query(lc[rt1],lc[rt2],l,mid,vl,vr)+query(rc[rt1],rc[rt2],mid+1,r,vl,vr);
}
int main()
{
int n,q;
scanf("%d %d",&n,&q);
char s[maxn];
for(int i=1;i<=n;i++)
{
scanf("%s",s);
insert(s,i);
}
getfail();
dfs(0);
int cur=0;//主席树的版本号
for(int i=1;i<=n;i++)
{
int rt=0;
for(int j=0;j<len[i];j++)
{
rt=tree[rt][sid[i][j]];cur++;
updata(root[cur],root[cur-1],1,dfsclock,in[rt]);
}
p[i]=root[cur];//保存更新完i串的版本
}
while(q--)
{
int l,r,x;
scanf("%d %d %d",&l,&r,&x);
printf("%d\n",query(p[r],p[l-1],1,dfsclock,in[endstr[x]],out[endstr[x]]));
}
return 0;
}