AC自动机

  常见的自动机:
  前缀树、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;
	}

}

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值