常见的自动机:
前缀树、KMP自动机、回文自动机、后缀自动机、AC自动机
AC自动机的构建过程:
首先给出一些敏感词,将其构建成前缀树
设置fail指针:0节点和其下一层节点的fail指针指向0节点;通过父亲节点和fail指针的跳转设置子节点的fail指针
查询:有一篇文章"dabcabcbcca",查询其敏感词的出现次数。设置times数组记录各节点的频率,遍历文章,只要有下一级节点,下一级节点和其fail指针不停跳转直至0节点路途中所有的节点全部加1;如果没有下一级节点就通过fail直至跳转至有下一级节点的节点
fail指针的含义:
其实就是fail指针起到了kmp算法中next数组的作用:只要匹配不成功就跳转到下一个有可能匹配成功的位置
AC自动机的优化:
优化fail指针的设置和匹配失败的移动:
fail指针的设置和匹配失败都会绕圈,为了优化省去绕圈,从将前缀树表改为直通表
如果有下级节点,从表中根据fail指针所指向节点的对应的路径的下级节点设置其fail指针;如果没有下级根据fail指针的节点对应路径所指向的节点填入表中。当进行匹配时,如果到某一个字符匹配不上,就根据直去表找到继续匹配的节点
遍历文章时的词频传递:遍历文章时,如果有对应的路径对应的节点就加1;如果没有就根据直去表对应的节点跳转。遍历完后建立每个节点和其fail指针的反图9(建立好后就是一棵树),遍历整颗树汇集每个子树的词频加到其父节点上,每个敏感词结尾的节点在收集到的次数就是对应敏感词出现的次数
P5357 【模板】AC 自动机 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
public class Code01_ACAM {
// 目标字符串的数量
public static int MAXN = 200001;
// 所有目标字符串的总字符数量
public static int MAXS = 200001;
// 记录每个目标串的结尾节点编号
public static int[] end = new int[MAXN];
// AC自动机
public static int[][] tree = new int[MAXS][26];
public static int[] fail = new int[MAXS];
public static int cnt = 0;
// 具体题目相关,本题为收集词频
// 所以每个节点记录词频
public static int[] times = new int[MAXS];
// 可以用作队列或者栈,一个容器而已
public static int[] box = new int[MAXS];
// 链式前向星,为了建立fail指针的反图
public static int[] head = new int[MAXS];
public static int[] next = new int[MAXS];
public static int[] to = new int[MAXS];
public static int edge = 0;
// 遍历fail反图,递归方法会爆栈,所以用非递归替代
public static boolean[] visited = new boolean[MAXS];
// AC自动机加入目标字符串
public static void insert(int i, String str) {
char[] s = str.toCharArray();
int u = 0;
for (int j = 0, c; j < s.length; j++) {
c = s[j] - 'a';
if (tree[u][c] == 0) {
tree[u][c] = ++cnt;
}
u = tree[u][c];
}
// 每个目标字符串的结尾节点编号
end[i] = u;
}
// 加入所有目标字符串之后
// 设置fail指针 以及 设置直接直通表
// 做了AC自动机固定的优化
public static void setFail() {
// box当做队列来使用
int l = 0;
int r = 0;
for (int i = 0; i <= 25; i++) {
if (tree[0][i] > 0) {
box[r++] = tree[0][i];
}
}
while (l < r) {
int u = box[l++];
for (int i = 0; i <= 25; i++) {
if (tree[u][i] == 0) {
tree[u][i] = tree[fail[u]][i];
} else {
fail[tree[u][i]] = tree[fail[u]][i];
box[r++] = tree[u][i];
}
}
}
}
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
int n = Integer.valueOf(in.readLine());
// AC自动机建树
for (int i = 1; i <= n; i++) {
insert(i, in.readLine());
}
setFail();
// 读入大文章
char[] s = in.readLine().toCharArray();
for (int u = 0, i = 0; i < s.length; i++) {
u = tree[u][s[i] - 'a'];
// 增加匹配次数
times[u]++;
}
for (int i = 1; i <= cnt; i++) {
// 根据fail指针建反图
// 其实是一颗树
addEdge(fail[i], i);
}
// 遍历fail指针建的树
// 汇总每个节点的词频
f2(0);
for (int i = 1; i <= n; i++) {
out.println(times[end[i]]);
}
out.flush();
out.close();
in.close();
}
public static void addEdge(int u, int v) {
next[++edge] = head[u];
head[u] = edge;
to[edge] = v;
}
// 逻辑是对的
// 但是递归开太多层了会爆栈
// C++这道题不会爆栈
// java会
public static void f1(int u) {
for (int i = head[u]; i > 0; i = next[i]) {
f1(to[i]);
times[u] += times[to[i]];
}
}
// 改成非递归才能通过
// 因为是用栈来模拟递归
// 只消耗内存空间(box和visited)
// 不消耗系统栈的空间
// 所以很安全
public static void f2(int u) {
// box当做栈来使用
int r = 0;
box[r++] = u;
int cur;
while (r > 0) {
cur = box[r - 1];
if (!visited[cur]) {
visited[cur] = true;
for (int i = head[cur]; i > 0; i = next[i]) {
box[r++] = to[i];
}
} else {
r--;
for (int i = head[cur]; i > 0; i = next[i]) {
times[cur] += times[to[i]];
}
}
}
}
}
当遍历到敏感词时及时报警优化:
设置fail指针时,将命中标记前移
P3311 [SDOI2014] 数数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
设置alert数组,只要来到某些触发警报的节点就结束
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
public class Code02_Counting {
public static int MOD = 1000000007;
// 目标字符串的数量
public static int MAXN = 1301;
// 所有目标字符串的总字符数量
public static int MAXS = 2001;
// 读入的数字
public static char[] num;
// 读入数字的长度
public static int n;
// AC自动机
public static int[][] tree = new int[MAXS][10];
public static int[] fail = new int[MAXS];
public static int cnt = 0;
// 具体题目相关,本题为命中任何目标串就直接报警
// 所以每个节点记录是否触发警报
public static boolean[] alert = new boolean[MAXS];
public static int[] queue = new int[MAXS];
// 动态规划表
public static int[][][][] dp = new int[MAXN][MAXS][2][2];
public static void clear() {
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= cnt; j++) {
dp[i][j][0][0] = -1;
dp[i][j][0][1] = -1;
dp[i][j][1][0] = -1;
dp[i][j][1][1] = -1;
}
}
}
// AC自动机加入目标字符串
public static void insert(String word) {
char[] w = word.toCharArray();
int u = 0;
for (int j = 0, c; j < w.length; j++) {
c = w[j] - '0';
if (tree[u][c] == 0) {
tree[u][c] = ++cnt;
}
u = tree[u][c];
}
alert[u] = true;
}
// 加入所有目标字符串之后
// 设置fail指针 以及 设置直接跳转支路
// 做了AC自动机固定的优化
// 做了命中标记前移防止绕圈的优化
public static void setFail() {
int l = 0;
int r = 0;
for (int i = 0; i <= 9; i++) {
if (tree[0][i] > 0) {
queue[r++] = tree[0][i];
}
}
while (l < r) {
int u = queue[l++];
for (int i = 0; i <= 9; i++) {
if (tree[u][i] == 0) {
tree[u][i] = tree[fail[u]][i];
} else {
fail[tree[u][i]] = tree[fail[u]][i];
queue[r++] = tree[u][i];
}
}
// 命中标记前移
alert[u] |= alert[fail[u]];
}
}
public static void main(String[] args) throws IOException {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
num = in.readLine().toCharArray();
n = num.length;
// AC自动机建树
int m = Integer.valueOf(in.readLine());
for (int i = 1; i <= m; i++) {
insert(in.readLine());
}
setFail();
// 清空动态规划表
clear();
// 执行记忆化搜索
out.println(f1(0, 0, 0, 0));
// out.println(f2(0, 0, 0, 0));
out.flush();
out.close();
in.close();
}
// 逻辑分支都详细列出来的版本
// i来到的位置
// j : AC自动机里来到的节点编号
// free : 是不是可以随意选择了
// free = 0,不能随意选择数字,要考虑当前数字的大小
// free = 1,能随意选择数字
// has : 之前有没有选择过数字
// has = 0,之前没选择过数字
// has = 1,之前选择过数字
// 返回i....幸运数字的个数
public static int f1(int i, int j, int free, int has) {
if (alert[j]) {
return 0;
}
if (i == n) {
return has;
}
if (dp[i][j][free][has] != -1) {
return dp[i][j][free][has];
}
int ans = 0;
int cur = num[i] - '0';
if (has == 0) {
if (free == 0) {
// 分支1 : 之前没有选择过数字 且 之前的决策等于num的前缀
// 能来到这里说明i一定是0位置, 那么cur必然不是0
// 当前选择不要数字
ans = (ans + f1(i + 1, 0, 1, 0)) % MOD;
// 当前选择的数字比cur小
for (int pick = 1; pick < cur; pick++) {
ans = (ans + f1(i + 1, tree[j][pick], 1, 1)) % MOD;
}
// 当前选择的数字为cur
ans = (ans + f1(i + 1, tree[j][cur], 0, 1)) % MOD;
} else {
// 分支2 : 之前没有选择过数字 且 之前的决策小于num的前缀
// 当前选择不要数字
ans = (ans + f1(i + 1, 0, 1, 0)) % MOD;
// 当前可以选择1~9
for (int pick = 1; pick <= 9; pick++) {
ans = (ans + f1(i + 1, tree[j][pick], 1, 1)) % MOD;
}
}
} else {
if (free == 0) {
// 分支3 : 之前已经选择过数字 且 之前的决策等于num的前缀
// 当前选择的数字比cur小
for (int pick = 0; pick < cur; pick++) {
ans = (ans + f1(i + 1, tree[j][pick], 1, 1)) % MOD;
}
// 当前选择的数字为cur
ans = (ans + f1(i + 1, tree[j][cur], 0, 1)) % MOD;
} else {
// 分支4 : 之前已经选择过数字 且 之前的决策小于num的前缀
// 当前可以选择0~9
for (int pick = 0; pick <= 9; pick++) {
ans = (ans + f1(i + 1, tree[j][pick], 1, 1)) % MOD;
}
}
}
dp[i][j][free][has] = ans;
return ans;
}
// 逻辑合并版
// 其实和f1方法完全一个意思
public static int f2(int i, int u, int free, int has) {
if (alert[u]) {
return 0;
}
if (i == n) {
return has;
}
if (dp[i][u][free][has] != -1) {
return dp[i][u][free][has];
}
int limit = free == 0 ? (num[i] - '0') : 9;
int ans = 0;
for (int pick = 0; pick <= limit; pick++) {
ans = (ans + f2(i + 1, has == 0 && pick == 0 ? 0 : tree[u][pick], free == 0 && pick == limit ? 0 : 1,
has == 0 && pick == 0 ? 0 : 1)) % MOD;
}
dp[i][u][free][has] = ans;
return ans;
}
}