Description
给定 (n) 个互不相同的字符串 (s_1, s_2,cdots s_n),一开始它们都包含在集合 (S) 中。
接下来有 (k) 个操作:
- 以
?
开头的操作为询问操作,给定字符串 (t),询问当前字符串集 (S) 中的每一个字符串匹配 (t) 的次数之和; - 以
+
开头的操作为添加操作,表示将字符串 (s_i) 加入到集合中(如果已经加了则忽略此操作); - 以
-
开头的操作为删除操作,表示将字符串 (s_i) 从集合中删除(如果 (S) 中本来就没有 (s_i) 则忽略此操作)。
Hint
- (1le n,kle 10^5)
- (1le sum |s_i| le 10^6)
Solution
多模式串的匹配问题,考虑 AC自动机。
首先看看 AC自动机 是如何工作的:走到一个结点,从这个结点一直往 fail
方向跳,边跳边统计答案,累加答案后继续往下一个字母的方向走……
这就是朴素的暴跳 fail
的方法。显然会被卡,跳一次 fail
都会卡成 (O(L)) ,这样一次询问下来就是 (O(L^2))。
接下来我们引入一个叫 fail树
的概念。如果一个结点 (x) (非根)的 fail
指向结点 (y),那么这个 fail树 上的结点 (x) 的父结点就是 (y)。最后建出的树即 fail树。因为每一个点只要一个 fail 边,根结点不算,所以边数等于结点数减一,保证是一颗合法的数。
然后我们回回顾一下上面那个跳 fail 的过程,其实就是 在 fail树 上跳祖先的过程。
这样就好办了,fail 路径上求和变成了 fail树 的链上求和。
如果没有修改操作,那么直接预处理根到结点的点权(点权即以该点为结尾的字符串的个数)和即可。这样复杂度是 (O(sum|s_i| + sum|t_i|))。
但是很不幸有了修改操作,不能草率地前缀和。我们知道,如果不用前缀和,那么修改就是 fail树 上的单点修改。
单点修改,链上查询……
使用 轻重链剖分 + 树状数组 !复杂度 (O(sum|s_i| + sum|t_i| imes log^2(sum|t_i|))),虽然是两个 (log),但是树状数组常数小,树剖跑不满,所以还是毫不费力地过了(摊手)。
但其实可以变成一个 (log),使用树上差分大法(wtcl不太会)。还有,可以使用神奇的 LCT 强行降成一个 (log),同时常数超级加倍最后只能极限卡常。
要注意,如果 AC自动机 的结点是从 0 开始的,记得树状数组那里加一,避免死循环。
Code
/*
* Author : _Wallace_
* Source : https://www.cnblogs.com/-Wallace-/
* Problem : Codeforces 163E e-Government
*/
#include <cstdlib>
#include <iostream>
#include <queue>
#include <string>
#include <vector>
using namespace std;
const int N = 1e5 + 5;
const int L = 1e6 + 5;
const int S = 26;
struct AC_Automaton { // AC自动机
struct Node {
int ch[S]; // 转移的边(自动机的转移函数)
int fail; // fail指针
int cnt; // 以当前的结尾的字符串的个数
} t[L];
int total; // 总结点数 (0-indexed)
inline int insert(string& s) { // 插入并返回结束位置
int x = 0;
for (string::iterator it = s.begin(); it != s.end(); it++) {
int c = *it - 'a';
if (!t[x].ch[c]) t[x].ch[c] = ++total;
x = t[x].ch[c];
}
++t[x].cnt;
return x;
}
inline void initFail() { // AC自动机建立fail指针
queue <int> Q;
for (register int c = 0; c < S; c++)
if (t[0].ch[c]) Q.push(t[0].ch[c]), t[t[0].ch[c]].fail = 0;
while (!Q.empty()) {
int x = Q.front(); Q.pop();
for (register int c = 0; c < S; c++)
if (t[x].ch[c]) {
Q.push(t[x].ch[c]);
t[t[x].ch[c]].fail = t[t[x].fail].ch[c];
} else t[x].ch[c] = t[t[x].fail].ch[c];
}
}
} acam;
int n, k;
string str[N];
int pos[N];
bool inSet[N];
namespace bit { // 树状数组
long long t[L];
int size = 0;
#define lbt(x) (x & (-x))
inline void inc(int p, int v) { // 单点加
for (; p <= size; p += lbt(p)) t[p] += v;
}
inline long long get(int p) { // 前缀和
long long ret = 0;
for (; p; p -= lbt(p)) ret += t[p];
return ret;
}
inline long long get(int l, int r) { // 区间和
return get(r) - get(l - 1);
}
#undef lbt
}
namespace Tree {
vector <int> G[L];
int fa[L], size[L], dep[L];
int maxs[L], top[L];
int timer = 0, dfn[L];
void __Dfs_1(int x) {
dep[x] = dep[fa[x]] + 1;
size[x] = 1, maxs[x] = 0;
for (vector <int>::iterator y = G[x].begin(); y != G[x].end(); y++) {
__Dfs_1(*y), size[x] += size[*y];
if (size[*y] > size[maxs[x]])
maxs[x] = *y;
}
}
void __Dfs_2(int x, int t) {
top[x] = t, dfn[x] = ++timer;
if (G[x].empty()) return;
__Dfs_2(maxs[x], t);
for (vector <int>::iterator y = G[x].begin(); y != G[x].end(); y++)
if (*y != maxs[x]) __Dfs_2(*y, *y);
}
inline void getFailTree(AC_Automaton& acam) {
for (register int i = 1; i <= acam.total; i++) {
G[acam.t[i].fail + 1].push_back(i + 1);
fa[i + 1] = acam.t[i].fail + 1;
} // 建fail树
bit::size = acam.total + 1;
__Dfs_1(1), __Dfs_2(1, 1); // 两个 DFS,处理树剖的信息
for (register int i = 0; i <= acam.total; i++)
bit::inc(dfn[i + 1], acam.t[i].cnt); // 初始化树状数组的信息
}
inline long long getChain(int x) { // 在根到 x 的路径上求和
long long ret = 0ll;
while (fa[top[x]]) {
ret += bit::get(dfn[top[x]], dfn[x]); // 累加答案
x = fa[top[x]]; // 跳链
}
ret += bit::get(dfn[x]); // 收尾
return ret;
}
};
inline long long query(string& s) {
int x = 0; long long ret = 0ll;
for (string::iterator it = s.begin(); it != s.end(); it++) { // 一路走一遍 AC 自动机
x = acam.t[x].ch[*it - 'a'];
ret += Tree::getChain(x + 1); // 树剖的链上求和
}
return ret;
}
inline void add(int idx) {
if (inSet[idx]) return; // 注意如果已经在集合里了就忽略
inSet[idx] = true; // 标记
bit::inc(Tree::dfn[pos[idx] + 1], +1); // 单点+1
}
inline void del(int idx) {
if (!inSet[idx]) return; // 注意如果本来就不在集合里就忽略
inSet[idx] = false; // 标记
bit::inc(Tree::dfn[pos[idx] + 1], -1); // 单点-1
}
signed main() {
ios::sync_with_stdio(false);
cin >> k >> n;
for (register int i = 1; i <= n; i++) {
cin >> str[i];
pos[i] = acam.insert(str[i]); // 插入模式串,记录 AC自动机中的结束位置
inSet[i] = true; // 一开始在集合里
}
acam.initFail(); // 处理 fail 指针
Tree::getFailTree(acam); // 建fail树,树剖
while (k--) {
string cmd;
cin >> cmd;
char opt = *cmd.begin();
cmd.erase(cmd.begin());
switch (opt) {
case '?' : // 询问
cout << query(cmd) << '
';
break;
case '+' : // 单点+1
add(atoi(cmd.c_str()));
break;
case '-' : // 单点-1
del(atoi(cmd.c_str()));
break;
}
}
}