「NOI2011」阿狸的打字机
知识点:ACAM,离线,树状数组
简述
建议先阅读原题面后再阅读简述题面。
通过奇怪的方法给定 \(n\) 个字符串 \(s_1\sim s_n\),给定 \(m\) 次询问。
每次询问给定参数 \(x\),\(y\),求在字符串 \(s_y\) 中 \(s_x\) 的出现次数。
\(1\le n,m,|\sum s_i|\le 10^5\)。
1S,256MB。
首先可以发现,题中给出的打字的过程与 Trie 的插入过程类似,由此可以直接构建出所有串的 Trie。
对 Trie 建立 ACAM 后,先考虑如何暴力查询。
对于每一次询问,都将字符串 \(s_y\) 扔到 ACAM 上匹配。每匹配到一个状态,就暴力上跳考察其在 \(\operatorname{fail}\) 树上的祖先中是否包含 \(s_x\) 对应状态。若包含则证明 \(s_x\) 作为当前匹配部分的一个后缀出现了,贡献累计即为答案。
总复杂度可以达到 \(O(T|\sum| + m|s_i|)\) 级别。其中 \(T\) 为 ACAM 节点数量,其上限为 \(\sum |s_i|\)。
注意到每次匹配的文本串都是模式串,这说明在匹配过程中,不会出现失配情况,且各状态不重复。即匹配过程中经过的路径是 Trie 中的一条自根向下的链。
观察暴力的过程,询问 \((x,y)\) 的答案即为祖先包括 \(s_x\) 状态的 \(s_y\) 的状态数。
由上述性质,这也可以理解为 \(\operatorname{fail}\) 树上祖先包括 \(s_x\) 的,自根至 \(s_y\) 的 Trie 上的链上的节点数量。
更具体地,考虑建立 \(\operatorname{fail}\) 树,答案为 \(s_x\) \(\operatorname{fail}\) 的子树中自根到 \(s_y\) 对应状态的链上的节点数量。
如何实现?对于询问 \((x,y)\),考虑大力标记 \(s_y\) 对应的所有状态,再查询 \(\operatorname{fail}\) 树上 \(s_x\) 的子树中被标记点数。上述过程可通过 dfn 序 + 树状数组完成。
如果对每次询问都做一次上面的过程,显然是非常浪费的。考虑离线所有询问,在每次询问的状态 \(s_y\) 上打一个询问 \(s_x\) 的标记。
之后在 Trie 上 dfs,每第一次访问到一个节点,就令树状数组中对应 dfn 位置 \(+1\),表示标记该节点。从该节点回溯时再 \(-1\)。
可以发现,dfs 到状态 \(u\) 时,被标记的节点恰好组成了自根至 \(s_y\) 的 Trie 上的链上的节点。则访问到 \(u\) 即可直接查询离线下来的询问。
总时间负责度 \(O(T|\sum| + m\log T)\),其中 \(T\) 为 ACAM 节点数量,其上限为 \(\sum |s_i|\)。
实现细节详见代码,注意映射关系。
代码
//知识点:ACAM,BIT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <queue>
#include <stack>
#include <vector>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int n, ans[kN], pos[kN];
char s[kN];
std::vector <int> query1[kN], query2[kN];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
namespace BIT {
#define low(x) (x&-x)
int Lim, t[kN];
void Init(int lim_) {
Lim = lim_;
}
void Insert(int pos_, int val_) {
for (int i = pos_; i <= Lim; i += low(i)) {
t[i] += val_;
}
}
int Sum(int pos_) {
int ret = 0;
for (int i = pos_; i; i -= low(i)) {
ret += t[i];
}
return ret;
}
int Query(int l_, int r_) {
return Sum(r_) - Sum(l_ - 1);
}
#undef low
}
namespace ACAM {
int node_num, fa[kN], tr[kN][26], fail[kN];
int e_num, head[kN], v[kN], ne[kN];
int dfn_num, dfn[kN], size[kN];
std::vector <int> trans[kN]; //原 Trie 树上的转移。因为建立了 Trie 图,需要把它记录下来,
void Read(char *s_) { //按照读入建立 Trie
int now = 0;
for(int i = 1, lim = strlen(s_ + 1); i <= lim; ++ i) {
if (s_[i] == 'P') {
pos[++ n] = now;
} else if (s_[i] == 'B') {
now = fa[now];
} else {
if (!tr[now][s_[i] - 'a']) {
tr[now][s_[i] - 'a'] = ++ node_num;
trans[now].push_back(node_num);
fa[node_num] = now;
}
now = tr[now][s_[i] - 'a'];
}
}
}
void Add(int u_, int v_) {
v[++ e_num] = v_;
ne[e_num] = head[u_];
head[u_] = e_num;
}
void Dfs(int u_) {
dfn[u_] = ++ dfn_num;
size[u_] = 1;
for (int i = head[u_]; i; i = ne[i]) {
int v_ = v[i];
Dfs(v_);
size[u_] += size[v_];
}
}
void Build(char *s_) {
Read(s_);
std::queue <int> q;
for (int i = 0; i < 26; ++ i) {
if (tr[0][i]) q.push(tr[0][i]);
}
while (! q.empty()) {
int now = q.front(); q.pop();
for (int i = 0; i < 26; ++ i) {
if (tr[now][i]) {
fail[tr[now][i]] = tr[fail[now]][i];
q.push(tr[now][i]);
} else {
tr[now][i] = tr[fail[now]][i];
}
}
}
for (int i = 1; i <= node_num; ++ i) Add(fail[i], i);
Dfs(0);
BIT::Init(node_num + 1);
}
void Query(int u_) { //dfs 回答询问到 u_
BIT::Insert(dfn[u_], 1); //标记
for (int i = 0, lim = query1[u_].size(); i < lim; ++ i) { //枚举此时可以回答的询问
int x = query1[u_][i], id = query2[u_][i]; //查询 x 的子树中标记点的个数
ans[id] = BIT::Query(dfn[x], dfn[x] + size[x] - 1);
}
for (int i = 0, lim = trans[u_].size(); i < lim; ++ i) Query(trans[u_][i]);
BIT::Insert(dfn[u_], -1); //去除标记
}
}
//=============================================================
int main() {
scanf("%s", s + 1);
ACAM::Build(s);
int m = read();
for (int i = 1; i <= m; ++ i) { //离线询问
int x = read(), y = read();
query1[pos[y]].push_back(pos[x]);
query2[pos[y]].push_back(i);
}
ACAM::Query(0);
for (int i = 1; i <= m; ++ i) printf("%d\n", ans[i]);
return 0;
}