前置知识:
Kmp自动机,字典树。
结构
AC自动机=kmp自动机+trie(好了恐怕有奆佬已经懂了)
AC自动机其实就是在字典树上Kmp匹配
先将所有的模式串建成一棵字典树, AC自动机的状态就对应字典树上的节点,转移就对应字典树的边(其实不止)。
现在要在字典树上求next,不过在AC自动机中,我们一般会叫它Fail。
对于一个状态u,fail[u]代表与根到其的路径上的字符连成的字符串的最长后缀是根到另一个节点路径连成的字符串。
对于一个转移$(p,c)$代表从状态$p$加一个字符$c$能够到达的最长的后缀与字典树上某个前缀的状态。如果字典树上本来就有这个转移那肯定直接指向其是最好的。
构造
先把字典树建出来,然后用bfs构造。
假设现在枚举到x节点,它有一个孩子y,那么要求y的fail,根据kmp自动机很容易想到是fail[x]的同样字符的孩子。
如果这个字符没有孩子那就指向和fail一样的节点。因为是bfs构造所以不会出现先后问题。
一般有两种写法:
void get_fail() { queue<int> q; while (!q.empty()) q.pop(); for (int i = 0; i < 26; i++) ch[0][i] = 1; fail[1] = 0; q.push(1); while (!q.empty()) { int p = q.front(); q.pop(); for (int i = 0; i < 26; i++) { if (ch[p][i]) { fail[ch[p][i]] = ch[fail[p]][i]; q.push(ch[p][i]); } else { ch[p][i] = ch[fail[p]][i]; } } } }
void get_fail() { queue<int> q; while (!q.empty()) q.pop(); for (int i = 0; i < 26; i++) {//先把与根相连的放进队列里 if (ch[1][i]) fail[ch[1][i]] =1, q.push(ch[1][i]); else ch[1][i] = 1; } while (!q.empty()) { int p = q.front(); q.pop(); for (int i = 0; i < 26; i++) { if (ch[p][i]) { fail[ch[p][i]] = ch[fail[p]][i]; q.push(ch[p][i]); } else { ch[p][i] = ch[fail[p]][i]; } } } }
基础应用
建好自动机后跑匹配,每次暴力跳fail,如果之前跳过就打上-1然后break
#include <bits/stdc++.h> using namespace std; const int N = 2000010; int n, m; int ans; char s[N]; int ch[N][26], tot = 1; int fail[N]; int End[N]; void insert() { int len = strlen(s + 1), p = 1; for (int i = 1; i <= len; i++) { if (!ch[p][s[i] - 'a']) ch[p][s[i] - 'a'] = ++tot; p = ch[p][s[i] - 'a']; } End[p]++; } void get_fail() { queue<int> q; while (!q.empty()) q.pop(); for (int i = 0; i < 26; i++) ch[0][i] = 1; fail[1] = 0; q.push(1); while (!q.empty()) { int p = q.front(); q.pop(); for (int i = 0; i < 26; i++) { if (ch[p][i]) { fail[ch[p][i]] = ch[fail[p]][i]; q.push(ch[p][i]); } else { ch[p][i] = ch[fail[p]][i]; } } } } int main() { scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%s", s + 1); insert(); } get_fail(); scanf("%s", s + 1); m = strlen(s + 1); int p = 1; for (int i = 1; i <= m; i++) { p = ch[p][s[i] - 'a']; for (int j = p; j && End[j] != -1; j = fail[j]) { ans += End[j]; End[j] = -1; } } printf("%d", ans); return 0; }
求每个点出现次数,可以在fail树上从下往上统计。
建树或拓扑都可。
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int N = 2000010; const int N2 = 200010; int n, m; int ch[N2][26], fail[N2], tot; ll cnt[N2]; char s[N]; int End[N2]; int in[N2]; void insert(int id) { int len = strlen(s + 1), p = 1; for (int i = 1; i <= len; i++) { if (!ch[p][s[i] - 'a']) ch[p][s[i] - 'a'] = ++tot; p = ch[p][s[i] - 'a']; } End[id] = p; } void get_fail() { queue<int> q; while (!q.empty()) q.pop(); for (int i = 0; i < 26; i++) ch[0][i] = 1; fail[1] = 0; q.push(1); while (!q.empty()) { int p = q.front(); q.pop(); for (int i = 0; i < 26; i++) { if (ch[p][i]) { fail[ch[p][i]] = ch[fail[p]][i]; q.push(ch[p][i]); } else { ch[p][i] = ch[fail[p]][i]; } } } } int main() { while (scanf("%d", &n) != EOF && n) { memset(in, 0, sizeof(in)); memset(End, 0, sizeof(End)); memset(cnt, 0, sizeof(cnt)); memset(ch, 0, sizeof(ch)); memset(fail, 0, sizeof(fail)); tot = 1; for (int i = 1; i <= n; i++) { scanf("%s", s + 1); insert(i); } get_fail(); scanf("%s", s + 1); m = strlen(s + 1); int p = 1; for (int i = 1; i <= m; i++) p = ch[p][s[i] - 'a'], cnt[p]++; for (int i = tot; i; i--) in[fail[i]]++; queue<int> q; while (!q.empty()) q.pop(); for (int i = 1; i <= tot; i++) if (in[i] == 0) q.push(i); while (!q.empty()) { p = q.front(); q.pop(); cnt[fail[p]] += cnt[p]; in[fail[p]]--; if (in[fail[p]] == 0) q.push(fail[p]); } for (int i = 1; i <= n; i++) { printf("%lld ", cnt[End[i]]); } } return 0; }
统计一个点有多少模式串是其后缀
直接在bfs时加上其fail的值即可。
while (!q.empty()) { p = q.front(); q.pop(); sum[p] = end[p] + sum[fail[p]]; for (int i = 0; i < 26; i++) { if (ch[p][i]) { fail[ch[p][i]] = ch[fail[p]][i]; q.push(ch[p][i]); } else { ch[p][i] = ch[fail[p]][i]; } } }
其它题目
[USACO12JAN]Video Game G(AC机+dp)
[JSOI2007]文本生成器(AC机+dp,反向考虑)
[POI2000]病毒(AC机+搜索)
CF1202E You Are Given Some Strings...
[NOI2011]阿狸的打字机
[HNOI2006]最短母串问题
CF710F String Set Queries(AC自动机+二进制分组)