阿狸的打字机(AC自动机经典题)
标签(空格分隔): ac自动机 fail树
概述:
写完这道题感觉整个人都升华了。。。断断续续写了三天。
题面:
题目描述 Description
阿狸喜欢收藏各种稀奇古怪的东西,最近他淘到一台老式的打字机。打字机 上只有 28 个按键,分别印有 26 个小写英文字母和'B'、'P'两个字母。 经阿狸研究发现,这个打字机是这样工作的:
输入小写字母,打字机的一个凹槽中会加入这个字母(按 P 前凹槽中至 少有一个字母)。
按一下印有'B'的按键,打字机凹槽中最后一个字母会消失。
按一下印有'P'的按键,打字机会在纸上打印出凹槽中现有的所有字母并 换行,但凹槽中的字母不会消失(保证凹槽中至少有一个字母)。
例如,阿狸输入 aPaPBbP,纸上被打印的字符如下: a aa ab 我们把纸上打印出来的字符串从 1 开始顺序编号,一直到 n。打字机有一个 非常有趣的功能,在打字机中暗藏一个带数字的小键盘,在小键盘上输入两个数 (x,y)(其中 1≤x,y≤n),打字机会显示第 x 个打印的字符串在第 y 个打印的字符串 中出现了多少次。 阿狸发现了这个功能以后很兴奋,他想写个程序完成同样的功能,你能帮助 他么?
输入描述 Input Description
输入的第一行包含一个字符串,按阿狸的输入顺序给出所有阿狸输入的字符。 第二行包含一个整数 m,表示询问个数。 接下来 m 行描述所有由小键盘输入的询问。其中第 i 行包含两个整数 x, y, 表示第 i 个询问为(x, y)。
输出描述 Output Description
输出 m 行,其中第 i 行包含一个整数,表示第 i 个询问的答案。
样例输入 Sample Input
aPaPBbP
3
1 2
1 3
2 3
样例输出 Sample Output
2
1
0
数据范围及提示 Data Size & Hint
\(1≤n≤ 1e5,1≤m≤ 1e5\)
详解:
题外话:2018年牛客多校的一题ac自动机,知道了补全trie图和普通的暴力会跳的fail指针的写法差别。两种都是对的,那一题有退格操作,每次退格都多一个字符串。暴力回跳的会超时。ac自动机的fail指针的求法有两种。
其一是在暴力回跳的版本。
inline void Build()
{
queue<int>que;
tree[0].fail = -1;
que.push(0);
while(!que.empty()) {
int now = que.front();
que.pop();
for(int i = 0; i < 26; i ++ ) {
if(tree[now].childs[i]) {
if(now == 0) {
tree[ tree[now].childs[i] ].fail = 0;
} else {
int v = tree[now].fail;
while(v != -1) {
if(tree[v].childs[i]) {
tree[ tree[now].childs[i] ].fail = tree[v].childs[i];
break;
}
v = tree[v].fail;
}
if(v == -1) {
tree[ tree[now].childs[i] ].fail == 0;
}
}
que.push(tree[now].childs[i]);
}
}
}
}
其二是本题写的版本
void get_fail() {
queue<int>que;
fail[root] = root;
for(int i = 0; i < 26; i++ ) {
if(ch[root][i] == -1) {
ch[root][i] = root;
} else {
fail[ ch[root][i] ] = root;
que.push(ch[root][i]);
}
}
while(!que.empty()) {
int now = que.front();
que.pop();
for(int i = 0; i < 26; i++ ) {
if(ch[now][i] == -1) {
ch[now][i] = ch[ fail[now] ][i];
} else {
fail[ ch[now][i] ] = ch[ fail[now] ][i];
que.push(ch[now][i]);
}
}
}
}
优劣也很明显,第二种避免了不断回跳造成的时间消耗。那题多校卡的就是这个。但是却破坏了字典树原有的结构。对于这题没有影响。
为了练习所以我写的是补全trie图的版本的自动机。
回归本题:
我们有一个暴力的思路。y结尾的字符串的fail指针指向x。那么根到x的字符串是,根到y结尾的字符串的后缀(fail指针的意义所在)。
那么我们对于每个答案,只要枚举根到y的路径上的每个结点。的fail指针如果经过若干次跳跃到了x。那么答案加加。这样显然太暴力。
我们可以反向思维。
fail指针反向建树。我们在root 到 y的路径全部加一。对于x。我们算子树中有多少个1即可。
维护子树我们需要dfs序 + 单点更新,区间求和的数据结构。这里用的是树状数组
代码:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e6 + 7;
struct BIT {
static const int MAXN = 1e6 + 7;
int c[MAXN];
inline void init() {
memset(c, 0, sizeof(c));
}
inline int lowbit(int x) {
return x & -x;
}
inline void add(int pos, int val) {
for(int i = pos; i < MAXN; i += lowbit(i) ) {
c[i] += val;
}
}
inline int sum(int pos) {
int ans = 0;
for(int i = pos; i > 0; i -= lowbit(i)) {
ans += c[i];
}
return ans;
}
} bit;
char buf[MAXN];
int lef[MAXN], rig[MAXN], dfs_index, ans[MAXN], vis[MAXN];
vector<pair<int, int> > query[MAXN]; /// q[y] = <x, id>
struct Node {
int to, w, next;
} edge[MAXN * 4];
int first[MAXN], sign;
void init() {
memset(first, -1, sizeof(first));
sign = 0;
}
void add_edge(int u, int v, int w) {
edge[sign].to = v;
edge[sign].w = w;
edge[sign].next = first[u];
first[u] = sign++;
}
int pos[MAXN];
struct ACauto {
static const int MAXN = 1e6 + 7;
int fail[MAXN], ch[MAXN][30], cnt[MAXN], tot, root, fa[MAXN];
int new_node() {
memset(ch[tot], -1, sizeof(ch[tot]));
fail[tot] = -1;
return tot++;
}
void init() {
tot = 0;
root = new_node();
}
void insert(char *str) {
int now = root, indexs = 0;
for(int i = 0; str[i]; i++ ) {
if(str[i] == 'P') {
pos[++indexs] = now;
} else if(str[i] == 'B') {
now = fa[now];
} else {
if(ch[now][ str[i] - 'a' ] == -1) {
ch[now][ str[i] - 'a' ] = new_node();
fa[ ch[now][ str[i] - 'a' ] ] = now;
}
now = ch[now][ str[i] - 'a' ];
}
}
}
void get_fail() {
queue<int>que;
fail[root] = root;
for(int i = 0; i < 26; i++ ) {
if(ch[root][i] == -1) {
ch[root][i] = root;
} else {
fail[ ch[root][i] ] = root;
que.push(ch[root][i]);
}
}
while(!que.empty()) {
int now = que.front();
que.pop();
for(int i = 0; i < 26; i++ ) {
if(ch[now][i] == -1) {
ch[now][i] = ch[ fail[now] ][i];
} else {
fail[ ch[now][i] ] = ch[ fail[now] ][i];
que.push(ch[now][i]);
}
}
}
for(int i = 1; i < tot; i++ ) {
add_edge(fail[i], i, 1);
}
}
void cal(char *str) {
int now = root, id = 0;
bit.add(lef[0], 1);
for(int i = 0; str[i]; i++ ) {
if(str[i] == 'P') {
id++;
for(int j = 0; j < query[id].size(); j++ ) {
int x = pos[query[id][j].first];
ans[ query[id][j].second ] = bit.sum(rig[x]) - bit.sum(lef[x] - 1);
}
} else if(str[i] == 'B') {
bit.add(lef[now], -1);
now = fa[now];
} else {
now = ch[now][ str[i] - 'a' ];
bit.add(lef[now], 1);
}
}
}
} ac;
void dfs1(int x) {
lef[x] = ++dfs_index;
for(int i = first[x]; ~i; i = edge[i].next) {
dfs1(edge[i].to);
}
rig[x] = dfs_index;
}
int main() {
bit.init();
ac.init();
init();
scanf("%s", buf);
ac.insert(buf);
ac.get_fail();
dfs1(0);
int q;
scanf("%d", &q);
for(int i = 1; i <= q; i++ ) {
int x, y;
scanf("%d %d", &x, &y);
query[y].push_back(make_pair(x, i));
}
ac.cal(buf);
for(int i = 1; i <= q; i++ ) {
printf("%d\n", ans[i]);
}
return 0;
}
/**
aPaPBbP
3
1 2
1 3
2 3
*/