ACAM

前置知识

字典树,\(kmp\)

约定&记号

\(t_u\):字典\(T\)的节点\(u\)所对应的字符串

\(pre_i\):字符串\(S\)长度为\(i\)的前缀

\(c_i\):1结点\(i\)上的字符

自动机

自动机可以解决字符串(广义)是否符合某种特定约束的模型

组成部分:

1 状态集合

2 传入字符串(广义)集合

3 转移函数

4 起始节点

5 有效转移状态集合

算法解决的问题

给定一个字典\(T\)和一个字符串\(S\),求\(T\)中各个单词在\(S\)中出现的次数

考虑字典中只有一个单词时,即求这一个单词在\(S\)中出现的次数,KMP显然可以解决。

kmp算法可以通过fail指针求解,那么我们可以考虑在字典树上也建立fail指针。具体的,在kmp中\(fail_i\)表示\(pre_i\)最长公共前后缀的长度,而ACAM的字典\(T\)中每个结点\(u\)对应的\(fail_u\)则直接表示\(t_u\)的最长的在字典\(T\)中的真后缀在\(T\)中的状态,这样方便转移。

fail指针实现

\(fa_u\)\(u\)的父亲,首先令\(fail_u\gets fail_{fa_u}\),若\(fail_{fa_u}\)没有\(c_i\)结点,则接着跳\(fail_u\gets fail_{fail_u}\),否则\(fail_u\gets fail_{fa_u}的c_i结点\)

转移函数:

为传入\(S\)做准备

\(f(u,c)\)为状态\(pre_u\)再加一个字符\(c\)得到的状态

\[f(u,c)=\begin{cases} u+1&S_{u+1}=c\\ 0&S_{u+1}\ne c \land u=0\\ f(fail_u,c)&S_u \ne c \land u \ne 0 \end{cases} \]

fail指针实现

queue <int> q;
r1(i,0,25)if( t[0][i] )q.push(t[0][i]);
while(!q.empty()){
	int u=q.front();q.pop();
	r1(i,0,25)
		if(t[u][i])fa[ t[u][i] ] = t[ fa[u] ][i],q.push(t[u][i]);
		else t[u][i]=t[ fa[u] ][i];
}

匹配细节

\(25\)\(CSP\_S\) 在这吃亏了。匹配的时候匹配成功了还要更新匹配成功的状态的在字典中出现的后缀的次数,直接暴力跳 \(fail\) 指针时间复杂度是错的,应该先把 \(fail\) 树建出来,做一下树上差分,最后再求一下前缀和

for(int i=1;i<=node_cnt;i++)G[fa[i]].push_back(i);//建树
...
void query(string s){ //匹配
	int p=0,ret=0;
	for(char c:s)p=t[p][c-'a'],sum[p]++;//差分
	dfs(0);
	r1(i,1,n)cout<<sum[id[i]]<<"\n";
}
...
void dfs(int u){for(auto v:G[u])dfs(v),sum[u]+=sum[v];}//前缀和

做题记录

\(I\) 【模板】AC 自动机P5357

模板题。

#include<iostream>
#include<cstring>
#include<queue>
#include<algorithm>
#include<vector>
#define gc getchar
#define N 200005
#define bar 233
#define pii pair<int,int>
#define nzn puts("***************")
#define inf 0x3f3f3f3f
#define S 5
#define r1(x,a,b) for(int x=a;x<=b;x++)
#define int long long
int rd(){
	int x=0,f=1;char c=gc();
	for(;!isdigit(c);c=gc())f=c==45?-1:f;
	for(; isdigit(c);c=gc())x=x*10+c-'0';
	return x*f;
}
using namespace std;
string opt;
int n;
int t[N][26],nod_cnt,fa[N],id[N],sum[N];
vector<int>G[N];
void insert(string x,int i){
	int p=0;
	for(char c:x){
		if(!t[p][c-'a'])t[p][c-'a']=++nod_cnt;
		p=t[p][c-'a'];
	}id[i]=p;
}
void build(){
	queue <int> q;
	r1(i,0,25)if( t[0][i] )q.push(t[0][i]);
	while(!q.empty()){
		int u=q.front();q.pop();
		r1(i,0,25)
		if(t[u][i])fa[ t[u][i] ] = t[ fa[u] ][i],q.push(t[u][i]);
		else t[u][i]=t[ fa[u] ][i];
	}
}
void dfs(int u){for(auto v:G[u])dfs(v),sum[u]+=sum[v];}
void query(string s){
	int p=0,ret=0;
	for(char c:s)p=t[p][c-'a'],sum[p]++;
	dfs(0);
	r1(i,1,n)cout<<sum[id[i]]<<"\n";
}
signed main(){ios::sync_with_stdio(0),cin.tie(0);
	cin>>n;
	r1(i,1,n)cin>>opt,insert(opt,i);
	build();
	r1(i,1,nod_cnt)G[fa[i]].push_back(i);
	cin>>opt;
	query(opt);
	return 0;
}

\(II\) P3121 [USACO15FEB] Censoring G

\(ACAM\) 与栈结合的一道题,把当前状态存到栈里,匹配成功就把这个单词弹出,回到这个单词前面一个的状态,答案也可以用栈记。

AC记录

\(III\) LG3966. [TJOI2013] 单词

挺板子的,建完 \(ACAM\) 每个单词都跑一次最后统计答案即可。

posted @ 2026-05-31 08:17  Sayhere  阅读(4)  评论(0)    收藏  举报