Loading

AC 自动机

概念

Aho Corasick Automaton,AC 自动机。

用于解决多模式串匹配问题。

思想

对给出的 \(n\) 个模式串建立一棵 Trie 树并在 Trie 树上建立若干条 fail 边。

然后在 Trie 树上沿着 fail 边路径进行匹配。

fail 指针定义为在结点 \(u\) 对应的字符串失配以后,可以立刻继续匹配的位置。

换言之 fail 指针指向 AC 自动机中该结点代表串的最长后缀。

假设当前需要求出 fail 指针的结点为 \(u\)\(u\) 的父结点 \(f\)\(u\) 的边权字母为 \(x\)。此时父结点的 fail 指针指向的子结点 \(v\) 就是结点 \(u\) 的 fail 指针所指向的结点。

考虑给每个结点虚拟出不存在的子结点,方便匹配的时候直接通过 fail 指针跳转。

假设当前需要求出 \(u\) 的子结点 \(v\) 的 fail 指针,但是子结点 \(v\) 并不存在。这时我们直接把 \(u\) 的子结点指向 \(u\) 的 fail 指针指向结点的边权字母相同的子结点。

套路

多串匹配

题目链接

建好 Trie 并处理好 fail 指针后直接跑 AC 自动机即可。

注意题目只需要我们求出每一个字符串是否在主串中出现过,因此我们可以在遍历完 Trie 树结点后将 Trie 树结点对应的模式串数量置为 \(- 1\)

遇到一个对应模式串数量为 \(- 1\) 的结点时,说明该结点以及其之后的 fail 路径都一定被访问并统计过了,可以直接退出。

#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;

const int maxn = 1e6 + 5;

struct node
{
	int fail, cnt;
	int son[26];
} tree[maxn];

int n, m;
int tot, ans;
char s[maxn];
queue<int> q;

void insert(char *s)
{
	int t = 0;
	m = strlen(s);
	for (int i = 0; i < m; i++)
	{
		int c = s[i] - 'a';
		if (!tree[t].son[c])
			tree[t].son[c] = ++tot;
		t = tree[t].son[c];
	}
	tree[t].cnt++;
}

void get_fail()
{
	for (int i = 0; i < 26; i++)
	{
		int t = tree[0].son[i];
		if (t)
		{
			tree[t].fail = 0;
			q.push(t);
		}
	}
	while (!q.empty())
	{
		int t = q.front();
		q.pop();
		for (int i = 0; i < 26; i++)
		{
			if (tree[t].son[i])
			{
				tree[tree[t].son[i]].fail = tree[tree[t].fail].son[i];
				q.push(tree[t].son[i]);
			}
			else
				tree[t].son[i] = tree[tree[t].fail].son[i];
		}
	}
}

void query()
{
	int now = 0;
	m = strlen(s);
	for (int i = 0; i < m; i++)
	{
		int c = s[i] - 'a';
		now = tree[now].son[c];
		for (int t = now; t && tree[t].cnt != -1; t = tree[t].fail)
		{
			ans += tree[t].cnt;
			tree[t].cnt = -1;
		}
	}
}

int main()
{
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
	{
		scanf("%s", s);
		insert(s);
	}
	get_fail();
	scanf("%s", s);
	query();
	printf("%d\n", ans);
	return 0;
}

拓扑优化

题目链接

这道题需要求出可能重复的 \(n\) 个模式串分别在主串中的出现次数。

给 Trie 树中的每一个结点维护一个 vector 数组表示该结点对应的若干个模式串编号。

采用类似 \(lazy\) 的思想,每次仅更新当前主串后缀对应的结点,不沿着 fail 边继续更新。

根据 fail 边建立出一棵 fail 树。显然在 fail 树中结点 \(u\) 对应的字符串一定是结点 \(u\) 的子结点对应的字符串的后缀。

换言之只需要查找 fail 树中结点 \(u\) 的子树内的模式串数量就可以求出结点 \(u\) 对应的模式串的出现次数。

#include <cstdio>
#include <cstring>
#include <vector>
#include <queue>
using namespace std;

const int maxn = 2e5 + 5;

struct node
{
	int cnt, fail;
	int son[26];
	vector<int> v; //结点 u 对应的若干模式串编号 
} tree[maxn];

struct Edge // fail 树 
{
	int to, nxt;
} edge[maxn * 2];

int n, m;
int cnt, tot;
int head[maxn], tar[maxn];
int size[maxn], ans[maxn];
bool vis[maxn];
char s[maxn * 10];
queue<int> q;

inline void add_edge(int u, int v)
{
	cnt++;
	edge[cnt].to = v;
	edge[cnt].nxt = head[u];
	head[u] = cnt;
}

inline void write(int x) // 本题轻度卡常 
{
	if (x < 0)
	{
		x = -x;
		putchar('-');
	}
	if (x > 9)
		write(x / 10);
	putchar(x % 10 + '0');
}

inline void insert(char *s, int id) // 插入模式串 
{
	int t = 0;
	m = strlen(s);
	for (register int i = 0; i < m; i++)
	{
		int c = s[i] - 'a';
		if (!tree[t].son[c])
			tree[t].son[c] = ++tot;
		t = tree[t].son[c];
	}
	tree[t].v.push_back(id);
	tar[id] = t;
}

inline void get_fail() // 求 fail 数组 
{
	for (register int i = 0; i < 26; i++)
	{
		if (tree[0].son[i])
		{
			tree[tree[0].son[i]].fail = 0;
			q.push(tree[0].son[i]);
		}
	}
	while (!q.empty())
	{
		int t = q.front();
		q.pop();
		add_edge(tree[t].fail, t);
		for (register int i = 0; i < 26; i++)
		{
			if (tree[t].son[i])
			{
				tree[tree[t].son[i]].fail = tree[tree[t].fail].son[i];
				q.push(tree[t].son[i]);
			}
			else
				tree[t].son[i] = tree[tree[t].fail].son[i];
		}
	}
}

inline void dfs(int u)
{
	int len = tree[u].v.size();
	size[u] = tree[u].cnt;
	for (int i = head[u]; i; i = edge[i].nxt) // 计算 u 子树内的模式串数量 
	{
		dfs(edge[i].to);
		size[u] += size[edge[i].to];
	}
	for (int i = 0; i < len; i++)
		ans[tree[u].v[i]] = size[u]; // 用 ans[i] 存储第 i 个模式串的出现次数 
}

inline int query()
{
	int now = 0;
	m = strlen(s);
	for (register int i = 0; i < m; i++)
	{
		now = tree[now].son[s[i] - 'a']; // 只更新主串后缀对应的结点 
		tree[now].cnt++;
	}
	dfs(0);
}

int main()
{
	scanf("%d", &n);
	for (register int i = 1; i <= n; i++)
	{
		scanf("%s", s);
		insert(s, i);
	}
	scanf("%s", s);
	get_fail();
	query();
	dfs(0);
	for (register int i = 1; i <= n; i++)
		write(ans[i]), putchar('\n');
	return 0;
}

随机游走

P6125 [JSOI2009] 有趣的游戏

考虑到在结尾添加字符类似 AC 自动机的转移,可以转化成在 AC 自动机上以一定概率随机游走,问最先到达某个终止结点的概率。

考虑在 AC 自动机上 dp,令 \(f[i]\) 为在 \(i\) 停止的概率,显然不能转移。

因为终止结点至多经过一次,所以期望的经过次数等于在此停止的概率。

求出结点之间转移的系数矩阵,然后高斯消元就行。

fail 树

fail 树定义为由 \(fail(u) \rightarrow u\) 构成的有根树。

注意到 fail 树的一些性质:

  • 结点的祖先都是其后缀

  • 结点一定被其子树中的结点包含(该结点是子树中任意结点的后缀)

这让我们联想到 SAM 的 parent tree,不卡常的情况下可以用上位 SAM.

  • P2336 [SCOI2012]喵星球上的点名

    有 AC 自动机做法,但是 SAM 草过去了,咕咕咕。

  • P5840 [COCI2015]Divljak

    首先对 \(S\) 建出 fail 树。

    对于将字符串加入 \(T\) 的操作,考虑给 \(T\) 的每个前缀对应的结点染上颜色,现在问题变成在 \(S_x\) 中数颜色。

    上面的问题可以转化成对每个前缀结点到根进行覆盖,考虑用树链求并的技术做。

  • P2414 [NOI2011] 阿狸的打字机

    考虑对 \(y\) 对应的结点到根的路径进行染色,于是只需要询问 \(x\) 的子树中被染色的结点总数。

    可以在 dfs 的时候做一些 trick,进来 + 1 出去 - 1,直接查询子树和就行。

posted @ 2021-08-09 21:10  kymru  阅读(258)  评论(0编辑  收藏  举报