CHY 自动机

CHY 自动机


『这个不是直接 AC 自动机吗?』

『怎么 AC 自动机?』

『搞棵 Trie 出来,然后在上面挂指针,拿节点去做数位 DP 就好了。』

『你怎么挂的指针?』

『就直接对每个串跑 KMP 挂指针。』

『这个不是 AC 自动机。』

『不是吗,AC 自动机不就是 KMP+Trie 吗?』

AC 自动机指针是挂在 Trie 树上 BFS 序小于自己的最长公共前后缀上的,你直接挂是暴力。』

『反正就是拿 Trie 树的节点去数位 DP 可以吧。』

And God said, Let there be automaton: and there was automaton.

正如上面所说,CHY 自动机与 AC 自动机的区别就在于失配指针所指向的节点,CHY 自动机的失配指针指向的一定是当前节点的祖先。

这会造成什么差异呢?


复杂度分析

AC 自动机按照 Trie 树的 BFS 序构建,而 CHY 自动机可以按照 Trie 树的 DFS 序构建。

构建 CHY 自动机的流程实际上就是一个 DFS 整棵 Trie 树并进行带撤销的 KMP

因为 KMP 的时间复杂度是均摊的,所以它的时间复杂度为 O(|s|)(与总串长成线性),空间复杂度为 O(|Σ||s|)(与 Tire 树点数成线性)。

相比之下,AC 自动机的时间复杂度与空间复杂度均为 O(|Σ||s|)(与 Trie 树点数成线性),逊色于 CHY 自动机。

为什么它们的时间复杂度会有差异呢?

考虑时间复杂度相同的广义后缀自动机,发现二者维护的均为文本串,而 AC 自动机维护的是模式串。

模式串在后缀自动机上匹配,不存在失配后的二次转移;但文本串在 AC 自动机上匹配,需要考虑失配后的转移,所以 AC 自动机的每个节点都必须建出规模为字符集大小的转移以保证时间复杂度。 若 CHY 自动机也对每个节点建出字符集大小的转移,可以得到与 AC 自动机同样的复杂度(与 Trie 树点数成线性)。

在不对字符集所有元素建出转移的前提下,CHY 自动机的空间瓶颈在于 Trie 树,所以我们可以使用 Hash 表再对 CHY 自动机空间复杂度进行优化,在选取模数优秀,字符集元素可以 O(1) 比较的前提下,其时间复杂度与空间复杂度均为 O(|s|)

为了减小码量,我们可以通过牺牲一下时间复杂度,用 STL 中的 map 替代 Hash 表,时间复杂度变为 O(f(Σ)|s|log(|s|))f(Σ) 表示字符集元素比较的时间复杂度,通常在 O2 优化下时间效率略胜于不加 O2 优化的 Hash 表做法,但空间占用大于 Hash 表做法。


应用

这里我们以 CHY 自动机模板题 为例。

给定 n 个文本串。

m 组询问,每组询问给出模式串 s,求 n 个文本串的前缀集合中有多少个串的 border 集合包含 s

数据范围:字符串均由小写字母构成,文本串与模式串总长均不超过 5×106

多模式多文本,至少一个插到 Trie 树上。

发现模式插到 Trie 上,AC 自动机带个 26,可以卡,考虑 CHY 自动机。

把文本插到 Trie 上,所有节点恰好表示 n 个文本串的前缀集合,答案就是模式串在 CHY 自动机的失配树上的子树大小 1,模式串失配则答案为 0

Code (map)
#include <bits/stdc++.h>
using namespace std;
#define MAXN (int)(5e6+233)
#define rep(i,l,r) for (int i=l;i<=r;i++)
#define mk make_pair

struct Trie
{
	
	int cnt,now,f[MAXN],lst[MAXN],pre[MAXN];
	char tr[MAXN];
	string t;
	vector<char> son[MAXN];
	map<pair<int,char>,int> to;
	
	inline void ins(string &s)
	{
		int x=0;
		for (auto c:s)
		{
			if (to.find(mk(x,c))!=to.end()) x=to[mk(x,c)];
			else son[x].push_back(c),tr[x=to[mk(x,c)]=++cnt]=c;
		}
	}
	
	void dfs(int x)
	{
		while (now&&tr[x]!=tr[lst[now]]) now=pre[now];
		if (x!=lst[0]&&tr[x]==tr[lst[now]]) now=lst[now];
		pre[x]=now;
		for (auto c:son[x]) lst[x]=to[mk(x,c)],dfs(lst[x]),now=pre[x];
		f[now]+=++f[x];
	}
	
	inline int qry(string &s)
	{
		int x=0;
		for (auto c:s)
		{
			if (to.find(mk(x,c))!=to.end()) x=to[mk(x,c)];
			else return 0;
		}
		return f[x]-1;
	}
	
} trie;

int n,m;
string s;

inline int read()
{
	int x=0,f=1;char c;
	while (!isdigit(c=getchar())) if (c=='-') f=-1;
	while (isdigit(c)) x=(x<<1)+(x<<3)+(c^48),c=getchar();
	return f*x;
}

signed main()
{
	n=read(),m=read();
	rep(i,1,n) cin>>s,trie.ins(s);
	trie.dfs(0);
	rep(i,1,m) cin>>s,printf("%d\n",trie.qry(s));
	return 0;
}
Code (hash)
#include <bits/stdc++.h>
using namespace std;
#define MAXN (int)(5e6+233)
#define rep(i,l,r) for (int i=l;i<=r;i++)
#define mk make_pair

const int P=5e6+11;

template <typename T,int (*F)(T)>
struct crq_map
{
	
	int cnt,head[P];
	
	struct Node
	{
		T x;
		int val,nex;
	} E[MAXN];
	
	inline int& operator [] (T p)
	{
		int x=F(p);
		for (int i=head[x];i;i=E[i].nex)
			if (E[i].x==p) return E[i].val;
		return E[++cnt]=(Node){p,-1,head[x]},head[x]=cnt,E[cnt].val;
	}
		
};

inline int crq_hash(pair<int,char> p) { return (p.second+p.first*1007+P)%P; }

struct Trie
{
	
	int cnt,now,f[MAXN],lst[MAXN],pre[MAXN];
	char tr[MAXN];
	vector<char> son[MAXN];
	crq_map<pair<int,char>,crq_hash> to;
	
	inline void ins(string &s,int x=0)
	{
		for (auto c:s)
		{
			int &y=to[mk(x,c)];
			if (y!=-1) x=y;
			else son[x].push_back(c),tr[x=y=++cnt]=c;
		}
	}
	
	void dfs(int x)
	{
		while (now&&tr[x]!=tr[lst[now]]) now=pre[now];
		if (x!=lst[0]&&tr[x]==tr[lst[now]]) now=lst[now];
		pre[x]=now;
		for (auto c:son[x]) lst[x]=to[mk(x,c)],dfs(lst[x]),now=pre[x];
		f[now]+=++f[x];
	}
	
	inline int qry(string &s,int x=0)
	{
		for (auto c:s)
		{
			int &y=to[mk(x,c)];
			if (y!=-1) x=y;
			else
			{
				to.head[crq_hash(mk(x,c))]=to.E[to.cnt].nex,to.cnt--;
				return 0;
			}
		}
		return f[x]-1;
	}
	
} trie;

int n,m;
string s;

signed main()
{
	scanf("%d%d",&n,&m);
	rep(i,1,n) cin>>s,trie.ins(s);
	trie.dfs(0);
	rep(i,1,m) cin>>s,printf("%d\n",trie.qry(s));
	return 0;
}
Code (array)
#include <bits/stdc++.h>
using namespace std;
#define MAXN (int)(5e6+233)
#define rep(i,l,r) for (int i=l;i<=r;i++)

struct Trie
{
	
	int cnt,now,top[MAXN],f[MAXN],lst[MAXN],pre[MAXN];
	char tr[MAXN];
	char son[MAXN][26];
	int to[MAXN][26];
	
	inline void ins(string &s,int x=0)
	{
		for (char c:s)
		{
			int &y=to[x][c-'a'];
			if (y) x=y;
			else son[x][++top[x]]=c,tr[x=y=++cnt]=c;
		}
	}
	
	void dfs(int x)
	{
		while (now&&tr[x]!=tr[lst[now]]) now=pre[now];
		if (x!=lst[0]&&tr[x]==tr[lst[now]]) now=lst[now];
		pre[x]=now;
		rep(i,1,top[x]) lst[x]=to[x][son[x][i]-'a'],dfs(lst[x]),now=pre[x];
		f[now]+=++f[x];
	}
	
	inline int qry(string &s,int x=0)
	{
		for (char c:s)
			if (to[x][c-'a']) x=to[x][c-'a'];
			else return 0;
		return f[x]-1;
	}
	
} trie;

int n,m;
string s;

signed main()
{
    ios::sync_with_stdio(false);
    cin>>n>>m;
	rep(i,1,n) cin>>s,trie.ins(s);
	trie.dfs(0);
	rep(i,1,m) cin>>s,printf("%d\n",trie.qry(s));
	return 0;
}

在洛谷评测计算动态内存的前提下,静态数组因为没有 vector 动态扩容的巨大常数而效率优势明显。

以及,关闭流同步的效率优化也很明显。

image

posted @   jzcrq  阅读(74)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示