零.胡扯
AC自动机? 自动AC机?别想多了,他只是一种字符串算法而已
好好好,进入主题
一.问题引入
我们知道kmp,哈希等等都是能够做单字符串匹配的
但是如果是多个串去匹配一个串呢?
先看个模板题 【模板】AC自动机
Q:能否做n次KMP?
A:可以是可以,但是你想想会不会TLE?
Q:那此题怎么做?
A:AC自动机?
Q:啥玩野啊
A:那就给你讲讲吧
二.算法概述
前置技能: T r i e + K M P Trie+KMP Trie+KMP
感性理解:AC自动机= T r i e + K M P Trie+KMP Trie+KMP
AC自动机是以Trie树为基础,结合KMP的思想建立的,常被用于多模式串的字符串匹配。
建立AC自动机分为两个步骤:
1.把所有的模式串构建在 T r i e Trie Trie上
2.对 T r i e Trie Trie树的每一个节点构建失配指针 f a i l fail fail
接下来我们分开考虑
三.构建Trie树
你Trie树怎么建AC自动机就怎么建
建完之后在尾部打个标记表示此处一个串结束了,方便答案统计
void insert(char *s) {
int n = strlen(s), now = 0 ;
for (int i = 0; i < n; i++) {
int x = s[i] - 'a' ;
if (!trie[now][x]) trie[now][x] = ++cnt ;
now = trie[now][x] ;
}
e[now]++ ;
}
四.构造失配( f a i l fail fail)指针
fail指针和kmp的next数组有很大的相似之处,两者同样是在失配的时候用于跳转的指针。
但是由于算法的目的不同,他们也有不同点:KMP要求的是后缀和前缀匹配,而AC自动机只需要相同后缀即可。
下面介绍 f a i l fail fail指针的基础思想
构建 f a i l fail fail 指针,可以参考 K M P KMP KMP中构造 n e x t next next数组的思想。
我们利用部分已经求出 f a i l fail fail 指针的结点推导出当前结点的 f a i l fail fail指针。具体我们用BFS实现:
考虑字典树中当前的节点 u u u, u u u的父节点是 p p p, p p p通过字符 c c c的边指向 u u u。
假设深度小于 u u u的所有节点的 f a i l fail fail指针都已求得。那么 p p p的 f a i l fail fail指针显然也已求得。
我们跳转到 p p p的 f a i l fail fail指针指向的结点 f a i l [ p ] fail[p] fail[p];
如果结点 f a i l [ p ] fail[p] fail[p] 通过字母 c c c 连接到的子结点 w w w 存在:
则让 u u u的 f a i l fail fail指针指向这个结点 w w w ( f a i l [ u ] = w fail[u]=w fail[u]=w)。
相当于在 p p p和 f a i l [ p ] fail[p] fail[p] 后面加一个字符 c c c,就构成了 f a i l [ u ] fail[u] fail[u] 。
如果 f a i l [ p ] fail[p] fail[p]通过字母 c c c 连接到的子结点 w w w 不存在:
那么我们继续找到 f a i l [ f a i l [ p ] ] fail[fail[p]] fail[fail[p]] 指针指向的结点,重复上述判断过程,一直跳 f a i l fail fail 指针直到根节点。
如果真的没有,就令 f a i l [ u ] = r o o t fail[u]=root fail[u]=root。
如此即完成了 f a i l fail fail指针的构建。
在构建 f a i l fail fail 指针的同时,我们也对 T r i e Trie Trie中模式串的结尾构建 f a i l fail fail 指针。这样在匹配到结尾后能自动跳转到下一个匹配项。
五.代码剖析
先看是如何通过BFS求出 f a i l fail fail指针的
void build() {
queue <int> q ;
clr(fail) ;
for (int i = 0; i < 26; i++) if (trie[0][i]) q.push(trie[0][i]) ;
while (!q.empty()) {
int now = q.front() ; q.pop() ;
for (int i = 0; i < 26; i++)
if (trie[now][i]){
fail[trie[now][i]] = trie[fail[now]][i] ;
q.push(trie[now][i]) ;
} else trie[now][i] = trie[fail[now]][i] ;
}
}
Q:为何不能直接把root丢进队列里?
A:如果这样干,根节点的子节点的 f a i l fail fail指针就变成了本身了
Q:为何没有 w h i l e while while就直接付给了 t r i e trie trie啊?
A: 字典树转字典图,能够更加方便的记录答案
再看怎么查询
int query(char *t) {
int now = 0, res = 0, n = strlen(t) ;
for (int i = 0; i < n; i++) {
now = trie[now][t[i] - 'a'] ;
for (int j = now; j && ~e[j]; j = fail[j]) res += e[j], e[j] = -1 ;
}
return res ;
}
声明
p
p
p作为字典树上当前匹配到的结点,
r
e
s
res
res即返回的答案
循环遍历匹配串,
p
p
p在字典树上跟踪当前字符。
利用
f
a
i
l
fail
fail 指针找出所有匹配的模式串,累加到答案中。然后清
0
0
0。
对
e
[
j
]
e[j]
e[j] 取反的操作用来判断
e
[
j
]
e[j]
e[j] 是否等于-1。
六.总结
到此,你已经理解了整个AC自动机的内容。我们一句话总结AC自动机的运行原理: 构建字典图实现自动跳转,构建失配指针实现多模式匹配。
#include <map>
#include <set>
#include <ctime>
#include <queue>
#include <stack>
#include <cmath>
#include <vector>
#include <bitset>
#include <cstdio>
#include <cctype>
#include <string>
#include <cstring>
#include <cassert>
#include <climits>
#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <functional>
using namespace std ;
#define rep(i, a, b) for (int (i) = (a); (i) <= (b); (i)++)
#define per(i, a, b) for (int (i) = (a); (i) >= (b); (i)--)
#define clr(a) memset(a, 0, sizeof(a))
#define ass(a, sum) memset(a, sum, sizeof(a))
#define lowbit(x) (x & -x)
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define enter cout << endl
#define siz(x) ((int)x.size())
typedef long long ll ;
typedef unsigned long long ull ;
typedef vector <int> vi ;
typedef pair <int, int> pii ;
typedef pair <ll, ll> pll ;
typedef map <int, int> mii ;
typedef map <string, int> msi ;
const int N = 1000010 ;
const int INF = 0x3f3f3f3f ;
const int iinf = 1e9 ;
const ll linf = 2e18 ;
const int MOD = 1000000007 ;
void print(int x) { cout << x << endl ; exit(0) ; }
void PRINT(string x) { cout << x << endl ; exit(0) ; }
void douout(double x){ printf("%lf\n", x + 0.0000000001) ; }
char s[N] ;
int T ;
struct ac {
int trie[N][26], e[N], fail[N], cnt = 0 ;
void insert(char *s) {
int n = strlen(s), now = 0 ;
for (int i = 0; i < n; i++) {
int x = s[i] - 'a' ;
if (!trie[now][x]) trie[now][x] = ++cnt ;
now = trie[now][x] ;
}
e[now]++ ;
}
void build() {
queue <int> q ;
clr(fail) ;
for (int i = 0; i < 26; i++) if (trie[0][i]) q.push(trie[0][i]) ;
while (!q.empty()) {
int now = q.front() ; q.pop() ;
for (int i = 0; i < 26; i++)
if (trie[now][i]){
fail[trie[now][i]] = trie[fail[now]][i] ;
q.push(trie[now][i]) ;
} else trie[now][i] = trie[fail[now]][i] ;
}
}
int query(char *t) {
int now = 0, res = 0, n = strlen(t) ;
for (int i = 0; i < n; i++) {
now = trie[now][t[i] - 'a'] ;
for (int j = now; j && ~e[j]; j = fail[j]) res += e[j], e[j] = -1 ;
}
return res ;
}
} HQG_AC ;
signed main() {
freopen("ac.in", "r", stdin) ;
freopen("ac.out", "w", stdout) ;
scanf("%d", &T) ;
while (T--) {
scanf("%s", s) ;
HQG_AC.insert(s) ;
}
HQG_AC.build() ;
scanf("%s", s) ; printf("%d\n", HQG_AC.query(s)) ;
}
七.例题
也还是差不多的,在结尾上标记编号,然后查询时如果 n o w now now是结尾,那么就答案+1,然后就排序输出即可,别忘清零
int n ;
char s[M][N], t[N] ;
struct node {
int pos, num ;
friend bool operator < (const node &a, const node &b) {
if (a.num != b.num) return a.num > b.num ;
else return a.pos < b.pos ;
}
} ans[N] ;
struct AC {
int trie[N][26], fail[N], end[N], cnt = 0 ;
void init(int x) {
clr(trie[x]) ;
fail[x] = 0, end[x] = 0 ;
}
void insert(char *s, int id) {
int now = 0, n = strlen(s) ;
for (int i = 0; i < n; i++) {
int c = s[i] - 'a' ;
if (!trie[now][c]) trie[now][c] = ++cnt, init(cnt) ;
now = trie[now][c] ;
}
end[now] = id ; // flag
}
void build() {
queue <int> q ; clr(fail) ;
for (int i = 0; i < 26; i++) if (trie[0][i]) q.push(trie[0][i]) ;
while (!q.empty()) {
int now = q.front() ; q.pop() ;
for (int i = 0; i < 26; i++)
if (trie[now][i]) {
fail[trie[now][i]] = trie[fail[now]][i] ;
q.push(trie[now][i]) ;
} else trie[now][i] = trie[fail[now]][i] ;
}
}
void query(char *s) {
int now = 0, n = strlen(s) ;
for (int i = 0; i < n; i++) {
int c = s[i] - 'a' ;
now = trie[now][c] ;
for (int j = now; j; j = fail[j]) ans[end[j]].num++ ; // calculate each strings
}
}
} my ;
signed main() {
while (scanf("%d", &n) && n) {
my.cnt = 0 ; my.init(0) ;
for (int i = 1; i <= n; i++) {
scanf("%s", s[i]) ;
ans[i] = (node) {i, 0} ;
my.insert(s[i], i) ;
}
my.build() ;
scanf("%s", t) ; my.query(t) ;
sort(ans + 1, ans + n + 1) ;
cout << ans[1].num << "\n" ; cout << s[ans[1].pos] << "\n" ;
for (int i = 2; i <= n; i++)
if (ans[i].num == ans[1].num) cout << s[ans[i].pos] << "\n" ;
else break ;
}
}
之后,这个题也就是银组的升级版
在结尾打个长度标记
然后栈操作。。。根那个题目差不多的吧,就是把 K M P − > A C KMP->AC KMP−>AC自动机
标记哪些有用哪些没用
int st[N], what[N] ;
int top, n ;
char s[N], t[N] ;
struct AC {
int trie[N][26], fail[N], end[N], cnt = 0 ;
void insert(char *s) {
int now = 0, n = strlen(s) ;
for (int i = 0; i < n; i++) {
int c = s[i] - 'a' ;
if (!trie[now][c]) trie[now][c] = ++cnt ;
now = trie[now][c] ;
}
end[now] = n ;
}
void build() {
queue <int> q ; clr(fail) ;
for (int i = 0; i < 26; i++) if (trie[0][i]) q.push(trie[0][i]) ;
while (!q.empty()) {
int now = q.front() ; q.pop() ;
for (int i = 0; i < 26; i++)
if (trie[now][i]){
fail[trie[now][i]] = trie[fail[now]][i] ;
q.push(trie[now][i]) ;
} else trie[now][i] = trie[fail[now]][i] ;
}
}
void solve(char *s) {
int now = 0, n = strlen(s) ;
for (int i = 0; i < n; i++) {
int c = s[i] - 'a' ;
now = trie[now][c] ;
st[++top] = now ;
what[top] = i ;
if (end[now]) { // it is one of the matching strings’end
top -= end[now] ;
if (!top) now = 0 ;
else now = st[top] ;
}
}
}
} HQG ;
signed main() {
scanf("%s", s) ;
scanf("%d", &n) ;
for (int j = 1; j <= n; j++) {
scanf("%s", t) ;
HQG.insert(t) ;
}
HQG.build() ; HQG.solve(s) ;
for (int i = 1; i <= top; i++) cout << s[what[i]] ; enter ;
}
多串去匹配多串? 你想多了
此题需要对 f a i l fail fail指针更加深入的了解
我们发现一个串出现的次数就是 f a i l fail fail树中该点的子树和
于是有了这个结论就可以过了
char s[N] ;
int sum ;
struct AC {
int trie[N][26], fail[N], a[N], h[N], sz[N], cnt ;
void insert(char *s, int x) {
int now = 0, n = strlen(s) ;
for (int i = 0; i < n; i++) {
int c = s[i] - 'a' ;
if (!trie[now][c]) trie[now][c] = ++cnt ;
now = trie[now][c] ;
sz[now]++ ;
}
a[x] = now ;
}
void build() {
int hd = 0, tl = 0 ;
clr(fail) ;
for (int i = 0; i < 26; i++) if (trie[0][i]) h[++tl] = trie[0][i] ;
while (hd < tl) {
int now = h[++hd] ;
for (int i = 0; i < 26; i++)
if (trie[now][i]) {
fail[trie[now][i]] = trie[fail[now]][i] ;
h[++tl] = trie[now][i] ;
} else trie[now][i] = trie[fail[now]][i] ;
}
}
void solve() {
for (int i = cnt; i >= 0; i--) sz[fail[h[i]]] += sz[h[i]] ;
for (int i = 1; i <= sum; i++) printf("%d\n", sz[a[i]]) ;
}
} my ;
signed main() {
scanf("%d", &sum) ;
for (int i = 1; i <= sum; i++) {
scanf("%s", s) ;
my.insert(s, i) ;
}
my.build() ; my.solve() ;
}
搞个差分数组就没了
原题这样就可以过了,这个题目听说要10~50个串后重建AC自动机,还是蛮巧妙的
原题AC的代码
int cf[N] ;
char s[N], t[N] ;
int n, m ;
struct AC {
int trie[N][26], fail[N], end[N], val[N], cnt = 0 ;
void insert(char *s) {
int now = 0, n = strlen(s) ;
for (int i = 0; i < n; i++) {
int c = s[i] - 'a' ;
if (!trie[now][c]) trie[now][c] = ++cnt ;
now = trie[now][c] ;
}
val[now] = n ;
}
void build() {
queue <int> q ; clr(fail) ;
for (int i = 0; i < 26; i++) if (trie[0][i]) q.push(trie[0][i]) ;
while (!q.empty()) {
int now = q.front() ; q.pop() ;
for (int i = 0; i < 26; i++)
if (trie[now][i]) {
int x = trie[now][i] ;
fail[x] = trie[fail[now]][i] ;
end[x] = val[fail[x]] ? fail[x] : end[fail[x]] ;
q.push(x) ;
} else trie[now][i] = trie[fail[now]][i] ;
}
}
int query(char *t) {
int now = 0, n = strlen(t) ;
for (int i = 0; i < n; i++) {
int c = t[i] - 'a' ;
while (now && !trie[now][c]) now = fail[now] ;
now = trie[now][c] ;
if (val[now]) cf[i - val[now] + 1]++, cf[i + 1]-- ; // make cf array
else cf[i - val[end[now]] + 1]++, cf[i + 1]-- ;
}
}
} my ;
signed main() {
scanf("%d", &n) ; scanf("%s", s) ; scanf("%d", &m) ;
for (int i = 1; i <= m; i++) {
scanf("%s", t) ;
my.insert(t) ;
}
my.build() ; my.query(s) ;
int now = 0, ans = 0 ;
for (int i = 0; i < n; i++) {
now += cf[i] ;
if (now) ans++ ;
}
printf("%d\n", n - ans) ;
}
还有些题,我会慢慢放的