AC自动机
(update on 2023.6.20)
前言:额,这个东西,难度有点超乎想象(noi级算法果然不是这么好学)(境界还是没达到),先沉淀一下吧,八月再咕(坐等八月update)
简介:在字典树上建立 fail 指针形成的一棵树,主要用于查询多字符串出现次数问题
首先是三个模板题:
最基础的模板题。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inf 0x7fffffff
const ll maxn=1000010;
struct node{ll son[27],fail,flag;}trie[maxn];
ll n,tot=1,ans;
char s[maxn];
inline void insert(char *s,ll root) {
ll len=strlen(s+1);
for (ll i=1;i<=len;++i) {
ll lett=s[i]-'a';
if (!trie[root].son[lett]) trie[root].son[lett]=++tot;
root=trie[root].son[lett];
}
trie[root].flag++;
}
inline void getfail() {
queue<ll> q;
for (ll i=0;i<26;++i) trie[0].son[i]=1;
q.push(1);
trie[1].fail=0;
while (!q.empty()) {
ll u=q.front();
q.pop();
for (ll i=0;i<26;++i) {
ll v=trie[u].son[i],Fail=trie[u].fail;
if (!v) {
trie[u].son[i]=trie[Fail].son[i];
continue;
}
trie[v].fail=trie[Fail].son[i];
q.push(v);
}
}
}
inline ll query(char *s,ll root) {
ll len=strlen(s+1);
for (ll i=1;i<=len;++i) {
ll v=s[i]-'a';
ll now=trie[root].son[v];//注意这里是用now来跳而不是用root
//trie[now].flag!=-1:加这个东西的原因是题目要求的是有多少个模式串出现,所以为优化复杂度加的
while (now>1&&trie[now].flag!=-1) {
ans+=trie[now].flag;
trie[now].flag=-1;//这个东西也是优化
now=trie[now].fail;
}
root=trie[root].son[v];
}
return ans;
}
inline ll in() {
char a=getchar();
ll t=0,f=1;
while(a<'0'||a>'9') {if (a=='-') f=-1;a=getchar();}
while(a>='0'&&a<='9') {t=(t<<1)+(t<<3)+a-'0';a=getchar();}
return t*f;
}
signed main() {
n=in();
for (ll i=1;i<=n;++i) {
scanf("%s",s+1);
insert(s,1);
}
getfail();
scanf("%s",s+1);
printf("%lld",query(s,1));
return 0;
}
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inf 0x7fffffff
const ll maxn=1000010;
struct node{ll son[27],fail,flag,id;}trie[100010];
ll n,tot=1,ans;
ll each[155];
char s[160][77];
inline void insert(char *s,ll root,ll num) {
ll len=strlen(s+1);
for (ll i=1;i<=len;++i) {
ll lett=s[i]-'a';
if (!trie[root].son[lett]) trie[root].son[lett]=++tot;
root=trie[root].son[lett];
}
trie[root].flag++;
if (trie[root].flag==1) trie[root].id=num;
//与模板一不一样的地方就是这里记录了一下id
}
inline void getfail() {
queue<ll> q;
for (ll i=0;i<26;++i) trie[0].son[i]=1;
q.push(1);
trie[1].fail=0;
while (!q.empty()) {
ll u=q.front();
q.pop();
for (ll i=0;i<26;++i) {
ll v=trie[u].son[i],Fail=trie[u].fail;
if (!v) {
trie[u].son[i]=trie[Fail].son[i];
continue;
}
trie[v].fail=trie[Fail].son[i];
q.push(v);
}
}
}
inline ll query(char *s,ll root) {
ll len=strlen(s+1);
for (ll i=1;i<=len;++i) {
ll v=s[i]-'a';
ll now=trie[root].son[v];
// 这个时候不再像模板一一样用flag去优化,因为每个模式串可能出现多次
//若想模板一一样写的话不能保证统计上每一次出现情况
while (now>1) {
if (trie[now].flag) each[trie[now].id]+=trie[now].flag;
now=trie[now].fail;
}
root=trie[root].son[v];
}
return ans;
}
inline void init() {
memset(each,0,sizeof(each));
memset(trie,0,sizeof(trie));
tot=1,ans=0;
}
inline ll in() {
char a=getchar();
ll t=0,f=1;
while(a<'0'||a>'9') {if (a=='-') f=-1;a=getchar();}
while(a>='0'&&a<='9') {t=(t<<1)+(t<<3)+a-'0';a=getchar();}
return t*f;
}
signed main() {
while (1) {
n=in();
if (!n) break;
init();
for (ll i=1;i<=n;++i) {
scanf("%s",s[i]+1);
insert(s[i],1,i);
}
getfail();
char g[maxn];
scanf("%s",g+1);
query(g,1);
for (ll i=1;i<=n;++i) ans=max(each[i],ans);
printf("%lld\n",ans);
for (ll i=1;i<=n;++i) if (each[i]==ans) printf("%s\n",s[i]+1);
}
return 0;
}
因为简单版我们在 query 的时候是一级一级暴力向上跳的,这样会导致浪费很多的复杂度(因为 fail 关系是一样且固定的,每次从一个节点上向上跳的时候都是只会跳固定的点)
观察到每个节点和他的 fail 满足类似父子关系,经过每个结点之后,因为我们要往上跳,一定会经过他的 fail,再经过他的 fail 的 fail。这产生了一种先后关系,所以我们为了满足这个先后关系可以将每个节点的 fail 节点建有向边,之后再跑拓扑排序。于是我们可以在每个 query 到的每个节点上打标记,在拓扑的时候把后到达的节点的权值加上先到达的结点的标记,这个节点新的标记值就是他的权值。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define inf 0x7fffffff
const ll maxn=2000010;
struct node{ll son[27],fail,flag,id,ans;}trie[maxn];
ll n,tot=1,ans;
ll each[2000010],du[maxn];
string s[2000010];
unordered_map<string,ll> Hash;
vector<ll> G[maxn];
inline void insert(string s,ll root,ll num) {
ll len=s.size();
for (ll i=0;i<len;++i) {
ll lett=s[i]-'a';
if (!trie[root].son[lett]) trie[root].son[lett]=++tot;
root=trie[root].son[lett];
}
trie[root].flag=1;
trie[root].id=num;
}
inline void getfail() {
queue<ll> q;
for (ll i=0;i<26;++i) trie[0].son[i]=1;
q.push(1);
trie[1].fail=0;
while (!q.empty()) {
ll u=q.front();
q.pop();
for (ll i=0;i<26;++i) {
ll v=trie[u].son[i],Fail=trie[u].fail;
if (!v) {
trie[u].son[i]=trie[Fail].son[i];
continue;
}
trie[v].fail=trie[Fail].son[i];
du[trie[v].fail]++;
q.push(v);
}
}
}
inline void topo() {
queue<ll> q;
for (ll i=1;i<=tot;++i) if (!du[i]) q.push(i);
while (!q.empty()) {
ll now=q.front();
q.pop();
if (trie[now].flag) each[trie[now].id]+=trie[now].ans;
ll v=trie[now].fail;
trie[v].ans+=trie[now].ans;
du[v]--;
if (!du[v]) q.push(v);
}
}
inline ll query(char *s,ll root) {
ll len=strlen(s+1);
for (ll i=1;i<=len;++i) {
ll lett=s[i]-'a';
root=trie[root].son[lett];
trie[root].ans++;
}
}
inline ll in() {
char a=getchar();
ll t=0,f=1;
while(a<'0'||a>'9') {if (a=='-') f=-1;a=getchar();}
while(a>='0'&&a<='9') {t=(t<<1)+(t<<3)+a-'0';a=getchar();}
return t*f;
}
signed main() {
n=in();
for (ll i=1;i<=n;++i) {
cin>>s[i];
insert(s[i],1,i);
}
getfail();
char g[maxn];
scanf("%s",g+1);
query(g,1);
topo();
for (ll i=1;i<=n;++i) Hash[s[i]]=max(each[i],Hash[s[i]]);
for (ll i=1;i<=n;++i) printf("%lld\n",Hash[s[i]]);
return 0;
}
ACAM的一些性质:
- 每个节点的 fail 节点为末尾的字符串一定是他的后缀
应用典例:You Are Given Some Strings...
考虑在文本串上枚举每一个中间点作为 \(s_i\) 和 \(s_j\) 的分界点,所以 \(s_i\) 就是文本串分界点左边(以下简称左文本串)的后缀,\(s_j\) 就是文本串分界点右边(以下简称右文本串)的前缀。因为 ACAM 每个节点的 fail 节点为末尾的字符串一定是他的后缀,所以我们可以在从左到右进行文本串查询操作的时候,把当前枚举到的作为左右分界点,于是当前节点能跳 fail 的次数就是当前分界点情况下的后缀个数即 \(s_i\) 个数。
处理出每个分界点情况下的 \(s_i\) 个数之后,因为我们接下来要求的是前缀,所以我们再反向建一次 ACAM,再和求后缀的流程一样再跑一次即可。
刷题笔记:
-
ACAM + dfs
题目中没有给出固定的文本串,所以我们在给出的文本矩阵上从边界处开始 dfs,将 dfs 到的作为当前文本串的一个字符,边 dfs 边匹配。
-
ACAM + 栈
和 [USACO15FEB] Censoring S原理一样
inline ll query(char *s,ll root) {
ll len=strlen(s+1);
for (ll i=1;i<=len;++i) {
ll v=s[i]-'a';
ll root=trie[root].son[v];
st[++top]=i;
pos[top]=root;
if (trie[root].flag) {
top-=trie[root].flag;
if (top<0) root=0;
else root=pos[top];
}
// now=trie[now].fail;
}
}
-
ACAM 上搜索
-
ACAM + dp
模板题:[JSOI2007]文本生成器
ACAM 的 dp 一般是有固定套路的,
大部分 \(f[i][j]\) 表示当前在节点 \(j\),且串长为i时的情况,
有时再加一维表示这个状态里面包含了哪些东西
而且 AC自动机 的 DP 会经常让你用矩阵乘法优化

浙公网安备 33010602011771号