初探AC自动机的fail树的应用(附2题)
最近学了学fail树,感觉挺傻逼高级的……
原来AC自动机的代码(getfail)长这样
queue<int> q;
inline void getfail() {
while(!q.empty()) q.pop();
fail[0] = 0;
for (int c=0; c<26; ++c) {
int p = ch[0][c];
if(p) {
fail[p] = lst[p] = 0;
q.push(p);
}
}
while(!q.empty()) {
int top = q.front(); q.pop();
for (int c=0; c<26; ++c) {
int p = ch[top][c];
if(!p) continue;
q.push(p);
int v = fail[top];
while(v && !ch[v][c]) v = fail[v];
fail[p] = ch[v][c];
lst[p] = isend[fail[p]] ? fail[p] : lst[fail[p]];
}
}
}
然后我们发现,我们可以在找的过程中把fail顺便赋上去,这样后来也方便。
只要改动2处地方即可QAQ
queue<int> q;
inline void getfail() {
while(!q.empty()) q.pop();
fail[0] = 0;
for (int c=0; c<26; ++c) {
int p = ch[0][c];
if(p) {
fail[p] = lst[p] = 0;
q.push(p);
}
}
while(!q.empty()) {
int top = q.front(); q.pop();
for (int c=0; c<26; ++c) {
int p = ch[top][c];
if(!p) {
ch[top][c]=ch[fail[top]][c];
continue;
}
q.push(p);
fail[p] = ch[fail[top]][c];
lst[p] = isend[fail[p]] ? fail[p] : lst[fail[p]];
}
}
}
然后呢,我们发现这样会比较快的处理出fail,而且对于dp的时候也更好用(这当然不是本文内容)
其实不说上面这些也是可以的。。。
因为我们发现,一个AC自动机节点的fail节点是其后缀,所以我们考虑把fail节点和当前节点连边这样就形成了一棵树对吧(根节点不要连自环)。那么这棵树叫做fail树。
那么fail树有啥用呢?
是xxx的后缀,而且AC自动机每个节点都表示一个字符串的一部分,那么如果走到了这个点,所有这个点代表的字符串的后缀都被走到啦!这有啥关系???fail树是把fail[p]和p连边。所以!就只要区间加法即可!把根到p的路径整体+1即可。那么查询呢??单点查询啊!什么你说你不会链的加???DFS序啊!
所以按照阿杜的说法,常态就是fail树+DFS序+RMQ。当然RMQ可以高级可以低级,有的时候还要LCT。
【例题】bzoj 3172 单词
这题已经被做烂了,我写了三遍了(SA,AC自动机,AC自动机+fail树)。。。
直接在插入的时候区间加即可。然后一遍dfs统计。
# include <stdio.h>
# include <algorithm>
# include <string.h>
# include <queue>
# include <vector>
// # include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
const int M = 1600010;
const int N = 27;
int ch[M][N], fail[M], cnt[M], sz=0, idx[M];
int isend[M];
char t[M];
# define next nxt
int head[M], next[M<<1], to[M<<1], tot=0;
inline void add(int u, int v) {
++tot; next[tot]=head[u]; head[u]=tot; to[tot]=v;
}
inline void insert(int j) {
int len = strlen(t), p=0;
for (int i=0; i<len; ++i) {
int c = t[i] - 'a';
if(!ch[p][c]) ch[p][c] = ++sz;
p = ch[p][c];
cnt[p] ++;
}
isend[p] ++;
idx[j] = p;
}
queue<int> q;
inline void getfail() {
while(!q.empty()) q.pop();
fail[0] = 0;
for (int c=0; c<26; ++c) {
int p = ch[0][c];
if(p) {
fail[p] = 0;
add(fail[p], p);
add(p, fail[p]);
q.push(p);
}
}
while(!q.empty()) {
int top = q.front(); q.pop();
for (int c=0; c<26; ++c) {
int p = ch[top][c];
if(!p) {
ch[top][c] = ch[fail[top]][c];
continue;
}
q.push(p);
int v = fail[top];
while(v && !ch[v][c]) v = fail[v];
fail[p] = ch[v][c];
add(fail[p], p);
add(p, fail[p]);
}
}
}
inline void dfs(int x, int fa) {
for (int i=head[x]; i; i=next[i]) {
if(to[i] == fa) continue;
dfs(to[i], x);
cnt[x] += cnt[to[i]];
}
}
int n, T;
int main() {
sz = 0;
scanf("%d", &n);
for (int i=1; i<=n; ++i) {
scanf("%s", t);
insert(i);
}
getfail();
dfs(0, -1);
for (int i=1; i<=n; ++i)
printf("%d\n", cnt[idx[i]]);
return 0;
}
【附】bzoj 2434 阿狸的打字机
这题多了'B'和'P',那么怎么办?
'B'相当于回到父节点。'P'打标记就行了(后面处理到询问的时候知道是哪一个标记在哪个节点上)
然后考虑询问x字符串在y字符串里出现的次数。那么我们可以考虑重现操作。
我们再“插入”一遍字符串,然后到'B'或正常字符的时候正常插入即可,并且修改点权(-1/+1)(这样我们就可以省去区间加操作了)。然后等到了y字符串处理完,对于每个y是当前y的询问,回答即可,回答的答案呢?就是x这棵子树的和啦(这应该很好理解吧,因为x子树是x的后缀,也可以包含y)。然后呢,就变成修改点,子树加的题目了!
什么?你不会?按照正常的DFS序+树状数组就行了啊!!!
2A。。模板敲错了一次。
# include <queue>
# include <vector>
# include <stdio.h>
# include <string.h>
# include <algorithm>
// # include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef long double ld;
typedef unsigned long long ull;
const int M = 500010;
const int N = 27;
int ch[M][N], fail[M], fa[M], sz=0;
int b[M], bn=0, pos[M];
queue<int> q;
vector< pair<int,int> > v[M];
char str[M];
# define next NEXT
int head[M], next[M], to[M], tot=0;
inline void add(int u, int v) {
++tot;
next[tot]=head[u];
head[u]=tot;
to[tot]=v;
}
inline void adde(int u, int v) {
add(u, v);
add(v, u);
}
inline void build() {
scanf("%s", str);
int p=0;
for (int i=0; str[i]; ++i) {
if(str[i] == 'B') p = fa[p];
else if(str[i] == 'P') {
b[p] = ++bn;
pos[bn] = p;
} else {
int c = str[i]-'a';
if(!ch[p][c]) ch[p][c]=++sz, fa[ch[p][c]] = p;
p = ch[p][c];
}
}
while(!q.empty()) q.pop();
fail[0] = 0;
for (int c=0; c<26; ++c) {
int p = ch[0][c];
if(p) fail[p] = 0, q.push(p);
}
while(!q.empty()) {
int top = q.front(); q.pop();
for (int c=0; c<26; ++c) {
int p = ch[top][c];
if(!p) {
ch[top][c] = ch[fail[top]][c];
continue;
}
q.push(p);
fail[p] = ch[fail[top]][c];
}
}
for (int i=0; i<=sz; ++i) adde(i, fail[i]);
}
int st[M], ed[M], DFN=0;
inline void dfs(int x, int fa) {
st[x] = ++DFN;
for (int i=head[x]; i; i=next[i]) {
if(to[i] == fa) continue;
dfs(to[i], x);
}
ed[x] = DFN;
}
int ans[M];
int c[M<<1];
inline int lowbit(int x) {
return x&(-x);
}
inline void change(int x, int del) {
for (; x<=DFN; x+=lowbit(x)) c[x]+=del;
}
inline int ask(int x) {
int ret=0;
for (; x; x-=lowbit(x)) ret+=c[x];
return ret;
}
inline int query(int l, int r) {
return ask(r)-ask(l-1);
}
int main() {
build(); dfs(0, -1);
int T; scanf("%d", &T);
for (int i=1; i<=T; ++i) {
int a, b; scanf("%d%d", &a, &b);
v[b].push_back(make_pair(a, i));
}
int p=0;
for (int i=0; str[i]; ++i) {
if(str[i] == 'P') {
int ps = b[p];
for (int j=0, jto=v[ps].size(); j<jto; ++j)
ans[v[ps][j].second] = query(st[pos[v[ps][j].first]], ed[pos[v[ps][j].first]]);
} else if(str[i] == 'B') {
change(st[p], -1);
p = fa[p];
} else {
int c = str[i] - 'a';
p = ch[p][c];
change(st[p], 1);
}
}
for (int i=1; i<=T; ++i)
printf("%d\n", ans[i]);
return 0;
}
2022年8月21日 18:59
Rajasthan Board Model Paper 2023 Class 5 Pdf Download with Answers for Rajasthani Medium, English Medium, Hindi Medium, Urdu Medium & Students for Small Answers, Long Answer, Very Long Answer Questions, and Essay Type Questions to Term1 & Term2 Exams at official website. RBSE Question Paper Class 5 New Exam Scheme or Question Pattern for Sammittive Assignment Exams (SA1 & SA2): Very Long Answer (VLA), Long Answer (LA), Small Answer (SA), Very Small Answer (VSA), Single Answer, Multiple Choice and etc.