【Codeforces710F】String Set Queries (强制在线)AC自动机 + 二进制分组
F. String Set Queries
You should process m queries over a set D of strings. Each query is one of three kinds:
- Add a string s to the set D. It is guaranteed that the string s was not added before.
- Delete a string s from the set D. It is guaranteed that the string s is in the set D.
- For the given string s find the number of occurrences of the strings from the set D. If some string p from D has several occurrences in s you should count all of them.
Note that you should solve the problem in online mode. It means that you can't read the whole input at once. You can read each query only after writing the answer for the last query of the third type. Use functions fflush in C++ and BufferedWriter.flush in Javalanguages after each writing in your program.
Input
The first line contains integer m (1 ≤ m ≤ 3·105) — the number of queries.
Each of the next m lines contains integer t (1 ≤ t ≤ 3) and nonempty string s — the kind of the query and the string to process. All strings consist of only lowercase English letters.
The sum of lengths of all strings in the input will not exceed 3·105.
Output
For each query of the third kind print the only integer c — the desired number of occurrences in the string s.
Examples
5
1 abc
3 abcabc
2 abc
1 aba
3 abababc
2
2
10
1 abc
1 bcd
1 abcd
3 abcd
2 abcd
3 abcd
2 bcd
3 abcd
2 abc
3 abcd
output
3
2
1
0
Solution
题目大意:给M个操作 1.插入一个串S 2.删除一个串S 3.给出一个串S,询问之前存在(插入后未被删除的)的串在S中出现的次数。
查询操作,显然是AC自动机随便做的,但是AC自动机不支持插入和删除。
不过可以考虑建多个AC自动机,查询时就全跑一边,这样显然是可以的,不过这样的复杂度明显是$O(N^{2})$的毫无意义。
但是建多个AC自动机是不可避免的了,不过即使这样时间复杂度完全可以从$O(N^{2})$优化下去,只需要优化建出的AC自动机数量就可以对总复杂度起到优化。
fail指针并不能快速合并,所以,只能暴力拆+暴力合,这种暴力拆解的合并,和启发式合并类似。
所以我们对这么多AC自动机按二进制分组,建出$logN$个AC自动机,这样时间复杂度就得到大幅优化。
这样实现起来其实也非常简单,用$root[i]$记录这个AC自动机的Trie树的根,顺带记录一下每个$root$的$size$,当当前的$root$的$size$和它前面的那个相同,就将他们合并,暴力拆小入大。
这样证明也和启发式合并一样,因为每个AC自动机最多被拆合$logN$次,所以复杂度是$O(NlogN)$
至于删除,比较麻烦的方法是在AC自动机上跑,然后把跑到的那个串的end标记-1。
但是,转化一下,我们所有删除的也按照上述方法处理建AC自动机,这样每次查询答案相减一下即可。
自己在写的时候有部分问题(常数优化)没及时注意到:
1.因为每次建AC自动机的时候,可能会进行合并,完全可以先进行合并再求一次fail指针,这样常数优化效果还是不错的。
2.在合并的时候,注意一下一边不存在时特判的写法,这里写的好坏,效率差距特别大!
优秀的写法:
自己愚蠢的写法:
Code
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<cmath> #include<queue> using namespace std; #define MAXN 300010 int Q,opt,len,now; char str[MAXN]; namespace ACMachine { struct Trie{int son[MAXN][27],fail[MAXN],end[MAXN],tim[MAXN],sz;}; struct ACM { #define ch str[i]-'a'+1 int root[MAXN],cnt,size[MAXN]; Trie trie; inline void Insert(int rt) { size[rt]++; now=rt; for (int i=1; i<=len; i++) if (trie.son[now][ch]) now=trie.son[now][ch]; else trie.son[now][ch]=++trie.sz,now=trie.sz; trie.end[now]=1; } inline void Buildfail(int x) { queue<int>q; q.push(x); while (!q.empty()) { now=q.front(); q.pop(); for (int i=1; i<=26; i++) if (trie.son[now][i]) { int fa=trie.fail[now]; while (fa && !trie.son[fa][i]) fa=trie.fail[fa]; trie.fail[trie.son[now][i]]=fa? trie.son[fa][i] : x; trie.tim[trie.son[now][i]]=trie.end[trie.son[now][i]]+trie.tim[trie.fail[trie.son[now][i]]]; q.push(trie.son[now][i]); } } } inline int Merge(int ls,int rs) { if (!ls || !rs) return (ls+rs); int rt=ls; if (!rt) return rt; trie.end[rt]=trie.end[ls]|trie.end[rs]; for (int i=1; i<=26; i++) trie.son[rt][i]=Merge(trie.son[ls][i],trie.son[rs][i]); return rt; } inline int Debug() { int re=0; now=root[cnt]; for (int i=1; i<=len; i++) { while (now && !trie.son[now][ch]) now=trie.fail[now]; now=trie.son[now][ch]; re+=trie.tim[now]; } printf("%d\n",re); } inline void MakeTrie() { root[++cnt]=++trie.sz; Insert(root[cnt]); while (cnt && size[cnt]==size[cnt-1]) root[cnt-1]=Merge(root[cnt-1],root[cnt]), size[cnt-1]+=size[cnt],size[cnt]=0,cnt--; Buildfail(root[cnt]); } inline int Query() { int re=0; for (int r=1; r<=cnt; r++) for (int i=1,now=root[r]; i<=len; i++) { while (now && !trie.son[now][ch]) now=trie.fail[now]; now=now? trie.son[now][ch] : root[r]; re+=trie.tim[now]; } return re; } }In,Out; } using namespace ACMachine; int main() { scanf("%d",&Q); while (Q--) { scanf("%d%s",&opt,str+1); len=strlen(str+1); switch (opt) { case 1: In.MakeTrie(); break; case 2: Out.MakeTrie(); break; case 3: printf("%d\n",In.Query()-Out.Query()); fflush(stdout); break; } } return 0; }
一开始写丑了,无限卡常....CF迟迟case18TLE....