CHY 自动机
\(\text{CHY}\) 自动机
『这个不是直接 \(\text{AC}\) 自动机吗?』
『怎么 \(\text{AC}\) 自动机?』
『搞棵 \(\text{Trie}\) 出来,然后在上面挂指针,拿节点去做数位 \(\text{DP}\) 就好了。』
『你怎么挂的指针?』
『就直接对每个串跑 \(\text{KMP}\) 挂指针。』
『这个不是 \(\text{AC}\) 自动机。』
『不是吗,\(\text{AC}\) 自动机不就是 \(\text{KMP+Trie}\) 吗?』
『\(\text{AC}\) 自动机指针是挂在 \(\text{Trie}\) 树上 \(\text{BFS}\) 序小于自己的最长公共前后缀上的,你直接挂是暴力。』
『反正就是拿 \(\text{Trie}\) 树的节点去数位 \(\text{DP}\) 可以吧。』
And God said, Let there be automaton: and there was automaton.
正如上面所说,\(\text{CHY}\) 自动机与 \(\text{AC}\) 自动机的区别就在于失配指针所指向的节点,\(\text{CHY}\) 自动机的失配指针指向的一定是当前节点的祖先。
这会造成什么差异呢?
复杂度分析
\(\text{AC}\) 自动机按照 \(\text{Trie}\) 树的 \(\text{BFS}\) 序构建,而 \(\text{CHY}\) 自动机可以按照 \(\text{Trie}\) 树的 \(\text{DFS}\) 序构建。
构建 \(\text{CHY}\) 自动机的流程实际上就是一个 \(\text{DFS}\) 整棵 \(\text{Trie}\) 树并进行带撤销的 \(\text{KMP}\)。
因为 \(\text{KMP}\) 的时间复杂度是均摊的,所以它的时间复杂度为 \(\mathcal{O}(\sum|s|)\)(与总串长成线性),空间复杂度为 \(\mathcal{O}(|\Sigma| \cdot \sum|s|)\)(与 \(\text{Tire}\) 树点数成线性)。
相比之下,\(\text{AC}\) 自动机的时间复杂度与空间复杂度均为 \(\mathcal{O}(|\Sigma| \cdot \sum|s|)\)(与 \(\text{Trie}\) 树点数成线性),逊色于 \(\text{CHY}\) 自动机。
为什么它们的时间复杂度会有差异呢?
考虑时间复杂度相同的广义后缀自动机,发现二者维护的均为文本串,而 \(\text{AC}\) 自动机维护的是模式串。
模式串在后缀自动机上匹配,不存在失配后的二次转移;但文本串在 \(\text{AC}\) 自动机上匹配,需要考虑失配后的转移,所以 \(\text{AC}\) 自动机的每个节点都必须建出规模为字符集大小的转移以保证时间复杂度。 若 \(\text{CHY}\) 自动机也对每个节点建出字符集大小的转移,可以得到与 \(\text{AC}\) 自动机同样的复杂度(与 \(\text{Trie}\) 树点数成线性)。
在不对字符集所有元素建出转移的前提下,\(\text{CHY}\) 自动机的空间瓶颈在于 \(\text{Trie}\) 树,所以我们可以使用 \(\text{Hash}\) 表再对 \(\text{CHY}\) 自动机空间复杂度进行优化,在选取模数优秀,字符集元素可以 \(\text{O}(1)\) 比较的前提下,其时间复杂度与空间复杂度均为 \(\mathcal{O}(\sum|s|)\)。
为了减小码量,我们可以通过牺牲一下时间复杂度,用 STL 中的 map
替代 \(\text{Hash}\) 表,时间复杂度变为 \(\mathcal{O}(f(\Sigma)\sum|s| \log (\sum|s|))\)。\(f(\Sigma)\) 表示字符集元素比较的时间复杂度,通常在 O2 优化下时间效率略胜于不加 O2 优化的 \(\text{Hash}\) 表做法,但空间占用大于 \(\text{Hash}\) 表做法。
应用
这里我们以 \(\text{CHY}\) 自动机模板题 为例。
给定 \(n\) 个文本串。
有 \(m\) 组询问,每组询问给出模式串 \(s\),求 \(n\) 个文本串的前缀集合中有多少个串的 \(\text{border}\) 集合包含 \(s\)。
数据范围:字符串均由小写字母构成,文本串与模式串总长均不超过 \(5 \times 10^6\)。
多模式多文本,至少一个插到 \(\text{Trie}\) 树上。
发现模式插到 \(\text{Trie}\) 上,\(\text{AC}\) 自动机带个 \(26\),可以卡,考虑 \(\text{CHY}\) 自动机。
把文本插到 \(\text{Trie}\) 上,所有节点恰好表示 \(n\) 个文本串的前缀集合,答案就是模式串在 \(\text{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
动态扩容的巨大常数而效率优势明显。
以及,关闭流同步的效率优化也很明显。