[NOI2011]阿狸的打字机
[NOI2011]阿狸的打字机(AC自动机,Fail树,离线,树状数组)
写题解前先放阿狸!
题目叙述
给你若干个字符串(输入方式奇特),求一个字符串在另一个字符串中出现了几次。
题解
-
首先构建
Trie
(如果题目不按这种方式输入的话,那么输入就会过多。。。 -
对于这个
Trie
建立AC
自动机。利用Fail
树的性质,一个字符串的后缀中有另一个字符串等价于在Fail
树中,一个节点的子树中有另一个节点(在Fail
树中)。也就是看一个点在Trie
中到根节点的路径上有多少个属于那个点的Fail
树。 -
也就相当于给一个节点到根节点的路径上都打上标记,看子树权值和。为了不多次打标记,考虑利用相邻两次打标记公共的部分,发现这是一个类似于虚树的东东,每次撤销一部分标记,再新增一部分。所以可以先按
dfs
序排序,然后打标记、撤标记即可。 -
但是发现并不需要这样,可以发现本质
dfs
,把这棵树dfs
一遍,到一个点可以处理出这个点到根节点打标记的情况。把关于这个点的所有询问都记下来,子树询问即可。具体使用树状数组。
代码
- 弄清楚是谁在谁的子树里查询!
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <vector>
#define lowbit(x) ((x)&(-(x)))
using namespace std;
const int maxLen = 1e5 + 5, maxQue = 1e5 + 5;
int len, nbQue, tot, wordId, dfstime, ch[maxLen][26], fa[maxLen], wordPos[maxLen], fail[maxLen];
int in[maxLen], out[maxLen], ans[maxLen], cpy[maxLen][26];
char str[maxLen];
queue<int> Q;
vector<int> tree[maxLen], ask[maxLen];
struct Query {
int mo, wenBen;
} que[maxQue];
struct Fenwick {
int sum[maxLen];
void Add(int pos, int val) {
for (; pos <= tot; pos += lowbit(pos))
sum[pos] += val;
}
int Pre(int pos) {
int ret = 0;
for (; pos; pos -= lowbit(pos)) ret += sum[pos];
return ret;
}
int Query(int lEP, int rEP) {
return Pre(rEP) - Pre(lEP - 1);
}
} sum;
void Bfs() {
fail[1] = 1;
for (int son = 0; son < 26; ++son) {
if (ch[1][son]) {
Q.push(ch[1][son]);
fail[ch[1][son]] = 1;
} else
ch[1][son] = 1;
}
while (!Q.empty()) {
int now = Q.front();
Q.pop();
for (int son = 0; son < 26; ++son) {
if (ch[now][son]) {
Q.push(ch[now][son]);
fail[ch[now][son]] = ch[fail[now]][son];
} else
ch[now][son] = ch[fail[now]][son];
}
}
}
void Dfs(int now) {
in[now] = ++dfstime;
for (auto to : tree[now])
Dfs(to);
out[now] = dfstime;
}
void Solve(int now) {
sum.Add(in[now], 1);
for (auto id : ask[now])
ans[id] = sum.Query(in[wordPos[que[id].mo]], out[wordPos[que[id].mo]]);
for (int son = 0; son < 26; ++son)
if (cpy[now][son])
Solve(cpy[now][son]);
sum.Add(in[now], -1);
}
int main() {
scanf("%s%d", str + 1, &nbQue);
tot = 1;
len = strlen(str + 1);
int now = 1;
for (int pos = 1; pos <= len; ++pos) {
if ('a' <= str[pos] && str[pos] <= 'z') {
if (!ch[now][str[pos] - 'a']) {
ch[now][str[pos] - 'a'] = ++tot;
fa[tot] = now;
}
now = ch[now][str[pos] - 'a'];
} else if (str[pos] == 'P') wordPos[++wordId] = now;
else now = fa[now];
}
memcpy(cpy, ch, sizeof(ch));
Bfs();
for (int i = 2; i <= tot; ++i) tree[fail[i]].push_back(i);
Dfs(1);
for (int i = 1; i <= nbQue; ++i) {
scanf("%d%d", &que[i].mo, &que[i].wenBen);
ask[wordPos[que[i].wenBen]].push_back(i);
}
Solve(1);
for (int i = 1; i <= nbQue; ++i) printf("%d\n", ans[i]);
return 0;
}
知识点
- 记录这种离线的处理方法(比如需要一个点到根节点都打上标记,那么就可以把询问挂在点上,遍历整棵树,遍历到一个节点时回答询问)。
- 记住这种
Fail
树的用法。