大家好,我是普及组选手,我又来写普及组题了
题意
你有一个 (map<string,int> a) 需要支持 (3) 种操作:
每次给定整数 (opt) 和字符串 (s),
若 (opt=1),则 (a[s]++);
若 (opt=2),则 (a[s]--);
若 (opt=3),则输出 (sum_t a[t] imes (t在s中出现的次数))。
强制在线,维护一个 (mask),初始时为 (0),每次操作的 (opt) 异或上 (mask),每次操作 (3) 后,设答案为 (ans),将 (mask) 异或上 (|ans|)。
(n,sum |s| le 10^6)
题解
文字游戏啊
原题 (3) 操作没有加那个括号,然后我看错题了,写了一发交上去考后发现爆 (0) 了
此后还要注意 (ans) 可能是负的(因为 (a[s]) 可以减成负数),而 (mask) 要异或上 (ans) 的绝对值!
喷完了,题解不想多说,没看错题的人应该都切了
显然强制在线是假的,没有加密字符串,所以可以预先对所有输入的字符串建 AC 自动机
然后在线扫一遍询问,问题就转化成了 AC 自动机上单点加/减 (1)、询问一条路径上每个点在 fail 树上到根的点权和之和
在 AC 自动机上跑一遍询问串,每走一个点就取其 fail 树上的链上权值和。树链剖分树状数组就行了
复杂度 (O(nlog n)),然而这个做法利用了没有加密字符串的漏洞
code
#include<bits/stdc++.h>
#define ll long long
#define N 1000005
using namespace std;
int n,op[N],len[N],bg[N],Len;
char s[N];
struct edge{int v,nxt;}e[N<<1];
int hd[N],cnt;
inline void add(int u, int v){e[++cnt]=(edge){v,hd[u]}, hd[u]=cnt;}
namespace ACA{
int ch[N][27],num[N],tot=1;
void ins(char *s, int id){
int u=1;
for(int i=0; i<len[id]; ++i){
int c=s[i]-'a';
if(!ch[u][c]) ch[u][c]=++tot;
u=ch[u][c];
}
num[id]=u;
}
int fail[N];
queue<int> Q;
void build(){
for(int i=0; i<26; ++i) ch[0][i]=1;
Q.push(1);
while(!Q.empty()){
int u=Q.front(); Q.pop();
for(int i=0; i<26; ++i)
if(!ch[u][i]) ch[u][i]=ch[fail[u]][i];
else fail[ch[u][i]]=ch[fail[u]][i], Q.push(ch[u][i]);
}
for(int i=2; i<=tot; ++i){
//cout<<fail[i]<<' '<<i<<endl;
add(fail[i],i);
}
}
}using namespace ACA;
int dfn[N],siz[N],idx;
void dfs(int u){
dfn[u]=++idx, siz[u]=1;
for(int i=hd[u]; i; i=e[i].nxt) dfs(e[i].v), siz[u]+=siz[e[i].v];
}
struct Fenwick{
int tr[N];
inline int lowbit(int x){return x&-x;}
inline void add(int i, int v){
for(; i<=idx; i+=lowbit(i)) tr[i]+=v;
}
inline int query(int i){
int ret=0;
for(; i>0; i-=lowbit(i)) ret+=tr[i];
return ret;
}
}fwk;
ll ans;
void query(int l, int r){
int u=1;
for(int i=l; i<=r; ++i){
int c=s[i]-'a';
u=ch[u][c];
ans+=fwk.query(dfn[u]);
}
//cout<<ans<<endl;
}
char str[N];
int main(){
scanf("%d",&n);
for(int i=1; i<=n; ++i){
scanf("%lld%s",&op[i],str);
len[i]=strlen(str);
bg[i]=Len+1;
ins(str,i);
for(int j=0; j<len[i]; ++j) s[++Len]=str[j];
}
build();
dfs(1);
int mask=0;
for(int i=1; i<=n; ++i){
op[i]^=mask;
if(op[i]==1)
fwk.add(dfn[num[i]],1),
fwk.add(dfn[num[i]]+siz[num[i]],-1);
else if(op[i]==2)
fwk.add(dfn[num[i]],-1),
fwk.add(dfn[num[i]]+siz[num[i]],1);
else{
ans=0;
//cout<<bg[i]<<' '<<len[i]<<endl;
query(bg[i],bg[i]+len[i]-1);
cout<<ans<<endl;
mask^=labs(ans);
}
}
return 0;
}
官方题解比较神仙,是这么说的:
用二进制分组建 (log n) 个 AC 自动机,记录每个 AC 自动机的 (size)。若有两个 AC 自动机的 (size) 相同就合并。和启发式合并、线段树类似,每个串最多被插入 (log n) 次,所以总时间复杂度是 (O(nlog n))。
这里详细地举个例子:你本来是对对每个 (1,2) 操作给出的 (s) 串建一个 AC 自动机(对于 (2) 操作,建一个终点权值为 (-1) 的 AC 自动机),但插入 (15) 个串之后,前 (8) 个串合并成了一个 AC 自动机,第 (9-12) 个串合并成了一个,第 (13-14) 个串被合并成了一个,第 (15) 个串合并成了一个。不难发现,加入第 (16) 个串时,它会和第 (15) 个串的 AC 自动机合并,再和第 (13-14) 个串的 AC 自动机合并,以此类推,最终第 (1) 到 (16) 个串的 AC 自动机合并成了一个。
AC 自动机合并的方法跟线段树合并一样。
然后我们考虑查询。查询时肯定还是要把 (s) 串放到每个 AC 自动机上匹配,累加经过的每个点在 fail 树上到根的点权和之和。
由于这种方法建出来的 AC 自动机是静态的,你在建树时可以直接 bfs 一遍,把 fail 树上的父亲对儿子的贡献给加到儿子上。这样查询的时候,经过的每个点上已经存好了该点在 fail 树上到根的点权和,无需再用数据结构动态维护。
- 建 AC 自动机的总复杂度是 (O(|s|log n));
- 合并两个 AC 自动机的复杂度是 (O(min(size[a],size[b])),用启发式合并的角度考虑,每个串的 AC 自动机最多被合并 (log n) 次,而每个串的 AC 自动机初始大小就是该串长度 (|s|),所以这部分的总复杂度 (O(sum|s|log n));
- 查询的总复杂度是 (O(sum|s|log n))(要在 (log) 个 AC 自动机上都跑一遍)。
(sum|s|) 与 (n) 同级,三部分复杂度相加,总复杂度 (O(nlog n))。
这个做法支持加密输入的串 (s),不过加密后好像期望答案就趋近于 (0) 了(因为字符集大小为 (26) 的情况下,随机出的两个串很难有谁包含谁的关系),所以 yww 供题时没加密输入的串,然后就被“离线”做法艹翻了……
code from 2019gzez01
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+10;
int val[MAXN];
long long mark;
struct acam
{
int ch[MAXN][26],f[MAXN],rt[MAXN],sz[MAXN],c[MAXN];
ll sum[MAXN];
queue<int> q;
int tot,cnt;
int insert(char *s,int delta)
{
rt[++cnt]=++tot;
_insert(rt[cnt],s,delta);
sz[cnt]=1;
while (cnt>=2&&sz[cnt]==sz[cnt-1])
{
rt[cnt-1]=merge(rt[cnt-1],rt[cnt]);
sz[cnt-1]*=2;
--cnt;
}
get_f(rt[cnt]);
}
int _insert(int root,char *s,int delta)
{
int now=root,len=strlen(s);
for (int i=0; i<len; now=ch[now][s[i]-'a'],++i)
if (!ch[now][s[i]-'a'])
ch[now][s[i]-'a']=++tot;
val[now]+=delta;
}
int merge(int x,int y)
{
if (!x||!y)
return x+y;
for (int i=0; i<26; ++i)
ch[x][i]=merge(ch[x][i],ch[y][i]);
val[x]+=val[y];
return x;
}
int get_f(int root)
{
int v,u;
c[0]=0;
f[root]=0;
for (int i=0; i<26; ++i)
if (ch[root][i]) q.push(ch[root][i]),f[ch[root][i]]=root;
while (!q.empty())
{
v=q.front();
q.pop();
c[++c[0]]=v;
for (int i=0; i<26; ++i)
{
if (!ch[v][i]) continue;
u=f[v];
while (u&&!ch[u][i]) u=f[u];
if (u)
f[ch[v][i]]=ch[u][i];
else f[ch[v][i]]=root;
q.push(ch[v][i]);
}
}
for (int i=1; i<=c[0]; ++i)
{
sum[c[i]]=val[c[i]];
if (f[c[i]]) sum[c[i]]+=sum[f[c[i]]];
}
}
int get_ans(char *s)
{
ll ans=0;
for (int i=1; i<=cnt; ++i)
ans+=_get_ans(s,rt[i]);
printf("%lld
",ans);
mark^=abs(ans);
}
ll _get_ans(char *s,int root)
{
ll ret=0;
int len=strlen(s),now=root,p;
for (int i=0; i<len; ++i)
{
p=s[i]-'a';
while (!ch[now][p]&&now)
now=f[now];
if (now)
now=ch[now][p];
else
now=root;
ret+=sum[now];
}
return ret;
}
} ac;
int n,m;
int main()
{
ll op;
int len;
char s[MAXN];
scanf("%d",&n);
mark=0;
for (int i=1; i<=n; ++i)
{
scanf("%d%s",&op,s);
op=op^mark;
if (op==1)
{
ac.insert(s,1);
}
else if (op==2)
{
ac.insert(s,-1);
}
else
{
ac.get_ans(s);
}
}
return 0;
}