AC 自动机

重新学 AC 自动机发现以前就像没见过一样……

首先是一段经典的话:“AC 自动机是 trie 树上跑 kmp
于是 AC 自动机的关键在于运用 nxt 进行匹配
由于这时的 nxt 形成一棵树形结构,可以将一些匹配问题转化为树上问题
如果 x 匹配到了文本串,那么所有 x 的祖先也都是可以匹配上的
那么经常可以通过统计祖先的前缀和来方便地维护
动态题目可以用树状数组等结构来维护子树信息

另外,为了加快失配匹配,可以建立虚儿子,于是匹配可以连续进行
至于具体的图就先不模拟了

一定注意一个大坑点是在这种 trie 图上父亲的特殊标记是要下传到儿子上的
注意父亲的编号不一定比儿子的小,因此这部分递推要写在 bfs 里,不能直接 for 循环!!!

另外补充一点,可以发现 AC 自动机的构建过程复杂度是要乘上字符集大小的,如果字符集较大,我们可以用主席树来构建,因为写出转移会发现大部分儿子都是从 nxti 继承过来的


AC 自动机上的 dp

AC 自动机上的 dp 一般都设 f[i][j] 表示长度为 i 的串结束于 j 节点的情况,那么转移相当于枚举所有的儿子进行移动

还有就是 dp 的过程中尽量通过改变自动机的样子来调整 dp,而不是无脑往上加状态
比如要求到达关键串停止,其实没有必要增加状态记录有没有到达关键串,而是直接把自动机上所有关键串的儿子指向自己即可


P3311 [SDOI2014] 数数

f[i][j][0/1] 表示长度为 i 且结束于 j 节点,卡不卡上限的方案数
那么模拟题意进行转移即可
对于前导零的情况可以初始化时给 f[len][trie[0][j]][0] 进行赋值

不合法的串建立 trie 树时顺便打标记,注意需要把 nxt[x] 的标记继承过来

代码
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
const int maxn=1205;
const int maxm=1505;
int n,m,trie[maxm][10],tot,q[maxm],f[maxn][maxm][2],ans,nxt[maxm];
bool vis[maxm];
char a[maxn],b[maxm];
void insert(char* str){
	int len=strlen(str+1),p=0;
	for(int i=1;i<=len;i++){
		int x=str[i]-'0';
		if(!trie[p][x])trie[p][x]=++tot;
		p=trie[p][x];
	}
	vis[p]=true;
	return ;
}
void build(){
	int hd=1,tl=0;
	for(int i=0;i<=9;i++){
		if(trie[0][i])q[++tl]=trie[0][i];
	}
	while(hd<=tl){
		int p=q[hd++];
		for(int i=0;i<=9;i++){
			int x=trie[p][i];
			if(!x)trie[p][i]=trie[nxt[p]][i];
			else{
				nxt[x]=trie[nxt[p]][i];
				vis[x]|=vis[nxt[x]];
				q[++tl]=x;
			}
		}
	}
	return ;
}
void modadd(int &a,int b){
	a=(a+b)%mod;
	return ;
}
int main(){
	scanf("%s",a+1);
	cin>>m;n=strlen(a+1);
	for(int i=1;i<=m;i++)scanf("%s",b+1),insert(b);
	build();
	for(int i=1;i<a[1]-'0';i++){
		if(!vis[trie[0][i]])f[1][trie[0][i]][0]++;
	}
	if(!vis[trie[0][a[1]-'0']])f[1][trie[0][a[1]-'0']][1]++;
	for(int i=2;i<=n;i++){
		for(int j=1;j<=9;j++){
			if(!vis[trie[0][j]])f[i][trie[0][j]][0]++;
		}
	}
	for(int i=1;i<=n-1;i++){
		for(int j=0;j<=tot;j++){
			for(int k=0;k<=9;k++)if(!vis[trie[j][k]])modadd(f[i+1][trie[j][k]][0],f[i][j][0]);
			for(int k=0;k<a[i+1]-'0';k++)if(!vis[trie[j][k]])modadd(f[i+1][trie[j][k]][0],f[i][j][1]);
			if(!vis[trie[j][a[i+1]-'0']])modadd(f[i+1][trie[j][a[i+1]-'0']][1],f[i][j][1]);
		}
	}
	for(int j=0;j<=tot;j++)modadd(ans,(f[n][j][0]+f[n][j][1])%mod);
	cout<<ans;
	return 0;
}

P3715 [BJOI2017]魔法咒语

这个分类讨论就过于阴间了……
直接考虑 L=2 的情况,发现从 f[i] 转移到 f[i+1]f[i+2]
而在 AC 自动机上的转移方程都是固定的
那么可以把这两行放进矩阵里进行转移

代码
#include<bits/stdc++.h>
using namespace std;
#define int long long

const int maxn=205,mod=1e9+7;
int n,m,l,len[maxn],trie[maxn][26],tot,vis[maxn],ans,nxt[maxn],trans[maxn][maxn];
char s[maxn][maxn],t[maxn];
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
void modadd(int &a,int b){a=(a+b)%mod;}
void insert(char *str){
	int len=strlen(str+1),p=0;
	for(int i=1;i<=len;i++){
		int x=str[i]-'a';
		if(!trie[p][x])trie[p][x]=++tot;
		p=trie[p][x];
	}
	vis[p]=true;
}
void build(){
	queue<int>q;
	for(int i=0;i<26;i++)if(trie[0][i])q.push(trie[0][i]);
	while(!q.empty()){
		int p=q.front();q.pop();
//		cout<<"hhh "<<p<<endl;
		for(int i=0;i<26;i++){
			if(trie[p][i])nxt[trie[p][i]]=trie[nxt[p]][i],q.push(trie[p][i]);
			else trie[p][i]=trie[nxt[p]][i];
		}
	}
	for(int i=0;i<=tot;i++)vis[i]|=vis[nxt[i]];
}
int con(int p,char *str){
	int len=strlen(str+1);
	for(int i=1;i<=len;i++){
		p=trie[p][str[i]-'a'];
		if(vis[p])return -1;
	}
	return p;
}
namespace p1{
	int f[maxn][maxn];
	void start(){
		f[0][0]=1;
		for(int i=0;i<=l;i++)for(int j=0;j<=tot;j++)if(f[i][j]){
			for(int k=1;k<=n;k++)if(i+len[k]<=l&&~trans[k][j])modadd(f[i+len[k]][trans[k][j]],f[i][j]);
		}
		for(int i=0;i<=tot;i++)modadd(ans,f[l][i]);
		cout<<ans;
	}
}
namespace p2{
	int all;
	struct Mat{
		int a[maxn][maxn];
		Mat(){memset(a,0,sizeof a);}
	}A,B;
	Mat operator * (Mat a,Mat b){
		Mat c;
		for(int i=1;i<=all;i++)for(int j=1;j<=all;j++){
			for(int k=1;k<=all;k++)c.a[i][j]+=a.a[i][k]*b.a[k][j]%mod;
			c.a[i][j]%=mod;
		}
		return c;
	}
	Mat po(Mat a,int b){
		Mat ans;for(int i=1;i<=all;i++)ans.a[i][i]=1;
		while(b){
			if(b&1)ans=ans*a;
			a=a*a;
			b>>=1;
		}
		return ans;
	}
	void print(Mat a){
		puts("print out a matrix");
		for(int i=1;i<=all;i++,puts(""))for(int j=1;j<=all;j++)cout<<a.a[i][j]<<" ";
		puts("");
	}
	void start(){
		tot++;all=tot*2;
		for(int i=0;i<tot;i++)if(!vis[i]){
			for(int j=1;j<=n;j++)if(~trans[j][i]){
				if(len[j]==1)B.a[i+1][trans[j][i]+1]++;
				else B.a[i+1][trans[j][i]+tot+1]++;//,B.a[i+1+tot][trans[j][i]+1+tot]++;
//				B.a[i+1+(len[j]==1?tot:1)][trans[j][i]+1+tot]++;
			}
		}
		for(int i=0;i<tot;i++)B.a[i+1+tot][i+1]++;
		A.a[1][1]=1;
		B=po(B,l);
		A=A*B;
//		print(A);
		for(int i=0;i<tot;i++)modadd(ans,A.a[1][i+1]);
		cout<<ans;
	}
}
signed main(){
	n=read(),m=read(),l=read();
	for(int i=1;i<=n;i++){
		scanf("%s",s[i]+1);
		len[i]=strlen(s[i]+1);
	}
	for(int i=1;i<=m;i++){
		scanf("%s",t+1);
		insert(t);
	}
	build();memset(trans,-1,sizeof trans);
	for(int i=1;i<=n;i++)for(int j=0;j<=tot;j++)if(!vis[j])trans[i][j]=con(j,s[i]);
	if(l<=100)return p1::start(),0;
	p2::start();
	return 0;
}

这道题 也是一样的道理,居然还是个黑题


结合树上问题

P2414 [NOI2011] 阿狸的打字机

首先插入操作都直接用于构建 AC 自动机即可
对于询问 (x,y) 的本质相当于求 fail 树上 x 的子树内有多少节点是 y 串内的
考虑 y 串在 trie 上走过的路径即是整个串中的节点
那么可以把询问离线,然后遍历整棵 trie 树,而在 fail 树上的子树关系通过树状数组维护 fail 树中的 dfs 序来得到

注意遍历 trie 树的时候不能直接用原来的 trie 数组,因为已经经过 AC 自动机的改造变成了一张图,需要备份

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
const int maxm=2e5+5;
#define pi pair<int,int>
#define fi first
#define se second
#define mp make_pair
int n,m,re[maxn],fa[maxn],son[maxn][26],trie[maxn][26],hd[maxn],cnt,q[maxn],x,y,ans[maxn],tot,dfn[maxn],en[maxn],lea[maxn],c[maxn],num,nxt[maxn];
vector<pi>ask[maxn];
char a[maxn];
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
struct Edge{
	int nxt,to;
}edge[maxm];
void add(int u,int v){
//	cout<<u<<" "<<v<<endl;
	edge[++cnt].nxt=hd[u];
	edge[cnt].to=v;
	hd[u]=cnt;
	return ;
}
void insert(){
	int p=0;
	for(int i=1;i<=n;i++){
		if(a[i]=='B')p=fa[p];
		else if(a[i]=='P')en[p]=++num,re[num]=p;//,cout<<"hhh "<<p<<" "<<en[p]<<endl;
		else{
			int x=a[i]-'a';
			son[p][x]=trie[p][x]=++tot;
			fa[tot]=p;
			p=trie[p][x];
		}
	}
	return ;
}
void build(){
	int hd=1,tl=0;
	for(int i=0;i<26;i++)if(trie[0][i])q[++tl]=trie[0][i],add(0,trie[0][i]);
	while(hd<=tl){
		int p=q[hd++];
		for(int i=0;i<26;i++){
			int x=trie[p][i];
			if(!x)trie[p][i]=trie[nxt[p]][i];
			else{
				nxt[x]=trie[nxt[p]][i];
				add(nxt[x],x);
				q[++tl]=x;
			}
		}
	}
	return ;
}
void dfs(int u){
//	cout<<u<<" ";
	dfn[u]=++tot;
	for(int i=hd[u];i;i=edge[i].nxt)dfs(edge[i].to);
	lea[u]=++tot;
	return ;
}
void add1(int x,int w){
	x++;for(;x<=tot+1;x+=x&-x)c[x]+=w;
	return ;
}
int ask1(int x){
	int res=0;x++;
	for(;x;x-=x&-x)res+=c[x];
	return res;
}
void dfs1(int u){
//	cout<<u<<" ";
	add1(dfn[u],1);
	for(int i=0;i<ask[u].size();i++){
		ans[ask[u][i].se]=ask1(lea[ask[u][i].fi])-ask1(dfn[ask[u][i].fi]-1);
	}
	for(int i=0;i<26;i++){
		if(son[u][i])dfs1(son[u][i]);
	}
	add1(dfn[u],-1);
	return ;
}
int main(){
//	freopen("1.in","r",stdin);
//	freopen("my.out","w",stdout);
	scanf("%s",a+1);n=strlen(a+1);
	insert();build();m=read();
	for(int i=1;i<=m;i++)x=read(),y=read(),ask[re[y]].push_back(mp(re[x],i));
	tot=0;dfs(0);dfs1(0);
	for(int i=1;i<=m;i++)printf("%d\n",ans[i]);
	return 0;
}

CF163E e-Government

一样的道理,每次在 fail 树上进行单点加和子树和的操作即可

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
const int maxm=1e6+5;
int n,m,hd[maxn],cnt,num,st[maxn],ed[maxn],id[maxn],q[maxn],trie[maxn][26],nxt[maxn],tot,c[maxn],ans;
char b[maxn],op[maxn];
bool vis[maxn];
struct Edge{
	int nxt,to;
}edge[maxm];
void add(int u,int v){
//	cout<<"hhh "<<u<<" "<<v<<endl;
	edge[++cnt].nxt=hd[u];
	edge[cnt].to=v;
	hd[u]=cnt;
	return ;
}
void insert(char* str,int num){
	int len=strlen(str+1),p=0;
	for(int i=1;i<=len;i++){
		int x=str[i]-'a';
		if(!trie[p][x])trie[p][x]=++tot;
		p=trie[p][x];
	}
	id[num]=p;
	return ;
}
void build(){
	int hd=1,tl=0;
	for(int i=0;i<26;i++){
		if(trie[0][i])q[++tl]=trie[0][i],add(0,trie[0][i]);
	}
	while(hd<=tl){
		int p=q[hd++];
		for(int i=0;i<26;i++){
			int x=trie[p][i];
			if(!x)trie[p][i]=trie[nxt[p]][i];
			else{
				nxt[x]=trie[nxt[p]][i];
				add(nxt[x],x);
				q[++tl]=x;
			}
		}
	}
	return ;
}
void dfs(int u){
	st[u]=++num;
	for(int i=hd[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		dfs(v);
	}
	ed[u]=num;
	return ;
}
int change(char* op){
	int len=strlen(op+1),res=0;
	for(int i=2;i<=len;i++)res=res*10+op[i]-'0';
	return res;
}
void add1(int x,int w){
	for(;x<=num;x+=x&-x)c[x]+=w;
	return ;
}
int ask1(int x){
	int res=0;
	for(;x;x-=x&-x)res+=c[x];
	return res;
}
void doadd(int l,int r,int w){
//	cout<<l<<" "<<r<<endl;
	add1(l,w),add1(r+1,-w);return ;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++)scanf("%s",b+1),insert(b,i);
	build();dfs(0);
//	for(int i=0;i<=tot;i++)cout<<st[i]<<" "<<ed[i]<<endl;
	for(int i=1;i<=m;i++)vis[i]=true,doadd(st[id[i]],ed[id[i]],1);
	for(int i=1;i<=n;i++){
		scanf("%s",op+1);
		if(op[1]=='+'){
			int x=change(op);
			if(!vis[x]){
				vis[x]=true;
				doadd(st[id[x]],ed[id[x]],1);
			}
		}
		if(op[1]=='-'){
			int x=change(op);
			if(vis[x]){
				vis[x]=false;
				doadd(st[id[x]],ed[id[x]],-1);
			}
		}
		if(op[1]=='?'){
			int len=strlen(op+1);ans=0;
			for(int i=2,j=0;i<=len;i++){
				int x=op[i]-'a';
				j=trie[j][x];
				ans+=ask1(st[j]);
//				cout<<"kkk "<<i<<" "<<j<<" "<<ask1(st[j])<<endl;
			}
			printf("%d\n",ans);
		}
	}
	return 0;
}

CF1437G Death DBMS

还是一样的套路,只不过变成了改变一个串的权值
一个串的权值在 fail 树上影响的范围仍然是一个区间,那么直接在线段树上开 multiset 来维护这个区间的权值集合,查询时拿出最大值,修改时先删后加入

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=3e5+5;
const int maxm=3e5+5;
int n,m,ans,trie[maxn][26],val[maxn],nxt[maxn],q[maxn],hd[maxn],cnt,tot,num,st[maxn],ed[maxn],en[maxn],w,x;
char a[maxn];
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
struct Edge{
	int nxt,to;
}edge[maxm];
void add(int u,int v){
//	cout<<"hhh "<<u<<" "<<v<<endl;
	edge[++cnt].nxt=hd[u];
	edge[cnt].to=v;
	hd[u]=cnt;
	return ;
}
struct Seg{
	int l,r;
	multiset<int>s;
}t[maxn*4];
void insert(char* str,int id){
	int len=strlen(str+1),p=0;
	for(int i=1;i<=len;i++){
		int x=str[i]-'a';
		if(!trie[p][x])trie[p][x]=++tot;
		p=trie[p][x];
	}
	en[id]=p;
	return ;
}
void build(){
	int hd=1,tl=0;
	for(int i=0;i<26;i++)if(trie[0][i])q[++tl]=trie[0][i],add(0,trie[0][i]);
	while(hd<=tl){
		int p=q[hd++];
		for(int i=0;i<26;i++){
			int x=trie[p][i];
			if(!x)trie[p][i]=trie[nxt[p]][i];
			else{
				nxt[x]=trie[nxt[p]][i];
				add(nxt[x],x);
				q[++tl]=x;
			}
		}
	}
	return ;
}
void dfs(int u){
	st[u]=++num;
	for(int i=hd[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		dfs(v);
	}
	ed[u]=num;
	return ;
}
void build(int p,int l,int r){
	t[p].l=l,t[p].r=r;
//	if(l==3&&r==3)cout<<" dfasfadf "<<p<<endl;
	if(l==r)return ;
	int mid=l+r>>1;
	build(p<<1,l,mid);
	build(p<<1|1,mid+1,r);
	return ;
}
void change(int p,int l,int r,int last,int w,int op){
//	if(t[p].l==1&&t[p].r==num)cout<<"change  "<<l<<" "<<r<<" "<<last<<" "<<w<<endl;
//	cout<<t[p].l<<" "<<t[p].r<<" "<<l<<" "<<r<<endl;
	if(t[p].l>=l&&t[p].r<=r){
		if(!op){
			t[p].s.insert(w);
			return ;
		}
		auto it=t[p].s.find(last);
		if(it!=t[p].s.end())t[p].s.erase(it);
		t[p].s.insert(w);
		return ;
	}
	int mid=t[p].l+t[p].r>>1;
	if(l<=mid)change(p<<1,l,r,last,w,op);
	if(r>mid)change(p<<1|1,l,r,last,w,op);
	return ;
}
void ask(int p,int pos){
	if(!t[p].s.empty())ans=max(ans,*--t[p].s.end());
	if(t[p].l==t[p].r)return ;
	int mid=t[p].l+t[p].r>>1;
	if(pos<=mid)ask(p<<1,pos);
	else ask(p<<1|1,pos);
	return ;
}
int main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++)scanf("%s",a+1),insert(a,i);
	build();dfs(0);build(1,1,num);
	for(int i=1;i<=n;i++){
		change(1,st[en[i]],ed[en[i]],0,0,0);
//		cout<<"ppp "<<en[i]<<" "<<st[en[i]]<<" "<<ed[en[i]]<<endl;
	}
	for(int i=1;i<=m;i++){
		int op=read();
		if(op==1){
			x=read(),w=read();
			change(1,st[en[x]],ed[en[x]],val[x],w,1);val[x]=w;
		}
		else{
			scanf("%s",a+1);ans=-1;
			int len=strlen(a+1);
			for(int i=1,j=0;i<=len;i++){
				int x=a[i]-'a';
				j=trie[j][x];
//				cout<<i<<"  "<<j<<endl;
				ask(1,st[j]);
			}
			printf("%d\n",ans);
		}
		/*
		cout<<"out:  ";
		for(auto it=t[5].s.begin();it!=t[5].s.end();it++){
			cout<<*it<<" ";
		}
		puts("");
		*/
	}
	return 0;
}

CF1483F Exam

非常神奇的一个题
首先明确枚举的是母串 t,然后寻找有多少符合条件的子串 s
对于一个 s 需要满足的条件其实是有两部分组成:能够作为满足条件 3 的子串;每次出现时都是满足条件 3
换句话说其在 t 中出现的次数是等于满足条件 3 出现的个数
第一个是自动机上用树状数组维护的基本操作,来看第二个怎么求
可以倒序枚举 t 的每一位,找到一个能匹配后缀的最靠前的串,并且不存在一个之前匹配的串的将其包含
其中最靠左的一个串可以预处理的时候顺便求出

以前写得有点儿抽象,重新来写一发
首先所有备选的串 s 一定是 fail 树上 t 代表的每一个节点上的其它字符串(这个结束位置要事先在 trie 图上往下转移一下)
考虑怎样选出这些字符串中满足第三个条件的那些
首先这些串之间不能有包含关系,也就是说如果在串上显式地被包含,就不能加入候选集合了
这个可以从前往后扫的时候维护出当前最靠后的位置,只把在这个位置之前的串加入
于是目前得到了一些互不相交的候选串
但是在这样的情况下仍然有可能被包含,那么我们就用上面提到的出现次数相等来判断
即这个串目前在候选集合中出现的次数=其在 t 串中出现的总次数即可

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
const int maxm=1e6+5;
const int inf=0x3f3f3f3f;
int n,m,ans,viscnt[maxn],c[maxn],len[maxn],trie[maxn][26],nxt[maxn],q[maxn],hd[maxn],cnt,tot,num,st[maxn],ed[maxn],en[maxn],w,x,endpos[maxn];
vector<int>vis,b[maxn];
char a[maxn];
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
struct Edge{
	int nxt,to;
}edge[maxm];
void add(int u,int v){
//	cout<<u<<" "<<v<<endl;
	edge[++cnt].nxt=hd[u];
	edge[cnt].to=v;
	hd[u]=cnt;
	return ;
}
void insert(char* str,int id){
	len[id]=strlen(str+1);
	int p=0;
	for(int i=1;i<=len[id];i++){
		int x=str[i]-'a';
		if(!trie[p][x])trie[p][x]=++tot;
		p=trie[p][x];
		b[id].push_back(p);
	}
	endpos[id]=p;
	en[p]=id;
	return ;
}
void build(){
	int hd=1,tl=0;
	for(int i=0;i<26;i++)if(trie[0][i])q[++tl]=trie[0][i],add(0,trie[0][i]);
	while(hd<=tl){
		int p=q[hd++];
		for(int i=0;i<26;i++){
			int x=trie[p][i];
			if(!x)trie[p][i]=trie[nxt[p]][i];
			else{
				nxt[x]=trie[nxt[p]][i];
				add(nxt[x],x);
				if(!en[x])en[x]=en[nxt[x]];
				q[++tl]=x;
			}
		}
	}
	return ;
}
void dfs(int u){
	st[u]=++num;
	for(int i=hd[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		dfs(v);
	}
	ed[u]=num;
	return ;
}
void add1(int x,int w){
	x++;for(;x<=num+1;x+=x&-x)c[x]+=w;
	return ;
}
int ask(int x){
	int res=0;x++;
	for(;x;x-=x&-x)res+=c[x];
	return res;
}
int main(){
	n=read();
	for(int i=1;i<=n;i++)scanf("%s",a+1),insert(a,i);
	build();dfs(0);
	for(int i=1;i<=n;i++){
		int last=inf;
		for(int j=b[i].size()-1;j>=0;j--){
			int x=b[i][j];
			if(j==b[i].size()-1)x=nxt[x];
			if(en[x]){
				if(j-len[en[x]]+1<last){
					last=j-len[en[x]]+1,vis.push_back(en[x]);
					viscnt[en[x]]++;
				}
			}
			add1(st[b[i][j]],1);
		}
		for(int j=0;j<vis.size();j++){
			int p=ask(ed[endpos[vis[j]]])-ask(st[endpos[vis[j]]]-1);
//			cout<<vis[j]<<" "<<p<<" "<<viscnt[vis[j]]<<" ";
			if(p==viscnt[vis[j]])ans++;
			viscnt[vis[j]]=0;
		}
//		puts("");
		vis.clear();
		for(int j=0;j<b[i].size();j++)add1(st[b[i][j]],-1);
	}
	cout<<ans;
	return 0;
}

非常规题

P2444 [POI2000]病毒

发现一个串能够延续本质相当于在 AC 自动机上有一个没被打过标记的儿子可以走过去,而无限延伸需要自动机上有一个没被标记的环即可

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=30005;
const int maxm=1000005;
int trie[maxn][26],nxt[maxn],n,tot,q[maxn];
char c[maxn];
bool vis[maxn],vis1[maxn],vis2[maxn];
void insert(char* str){
	int len=strlen(str+1),p=0;
	for(int i=1;i<=len;i++){
		int x=str[i]-'0';
		if(!trie[p][x]){
			trie[p][x]=++tot;
		}
		p=trie[p][x];
	}
	vis[p]=true;
	return ;
}
void build(){
	int hd=1,tl=0;
	for(int i=0;i<2;i++){
		if(trie[0][i]){
			q[++tl]=trie[0][i];
		}
	}
	while(hd<=tl){
		int t=q[hd++];
		for(int i=0;i<2;i++){
			int x=trie[t][i];
			if(!x)trie[t][i]=trie[nxt[t]][i];
			else{
				nxt[x]=trie[nxt[t]][i];
				q[++tl]=x;
				vis[x]|=vis[nxt[x]]; 
			}
		}
	}
	return ;
}
void dfs(int u){
	if(vis1[u]){
		cout<<"TAK";
		exit(0);
	}
	if(vis[u]||vis2[u])return ;
	vis1[u]=true;
	vis2[u]=true;
	for(int i=0;i<=1;i++){
		int v=trie[u][i]; 
		dfs(v);
	}
	vis1[u]=false;
	return ;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		scanf("%s",c+1);
		insert(c);
	}
	build();
	dfs(0);
	cout<<"NIE";
	return 0;
}

CF1202E You Are Given Some Strings...

第一步就没有想到……
发现题目要求的是拼接 s 去匹配 t,那么可以拆分 t 来匹配 s
枚举 t 上的一个断点,断点前的一个后缀匹配一个 s,断点后的一个前缀匹配一个 s,那么两部分都转化为 AC 自动机可以维护的经典问题

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int n,m,f1[maxn],f2[maxn];
long long ans;
char a[maxn],b[maxn];
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
struct ACAM{
	int trie[maxn][26],tot,q[maxn],nxt[maxn],cnt[maxn];
	void insert(char* str){
		int len=strlen(str+1),p=0;
		for(int i=1;i<=len;i++){
			int x=str[i]-'a';
			if(!trie[p][x])trie[p][x]=++tot;
			p=trie[p][x];
		}
		cnt[p]++;
		return ;
	}
	void build(){
		int hd=1,tl=0;
		for(int i=0;i<26;i++){
			if(trie[0][i])q[++tl]=trie[0][i];
		}
		while(hd<=tl){
			int p=q[hd++];
			for(int i=0;i<26;i++){
				int x=trie[p][i];
				if(!x)trie[p][i]=trie[nxt[p]][i];
				else{
					nxt[x]=trie[nxt[p]][i];
					cnt[x]+=cnt[nxt[x]];
//					cout<<x<<" "<<cnt[x]<<endl;
					q[++tl]=x;
				}
			}
		}
		return ;
	}
	void ask(char* str,int* f){
		for(int i=1,j=0;i<=n;i++){
			int x=str[i]-'a';
			j=trie[j][x];
			f[i]=cnt[j];
		}
		return ;
	}
}pre,suf;
int main(){
	scanf("%s",a+1);
	n=strlen(a+1);m=read();
	for(int i=1;i<=m;i++){
		scanf("%s",b+1);
		pre.insert(b);
		int len=strlen(b+1);
		reverse(b+1,b+len+1);
		suf.insert(b);
	}
	pre.build();suf.build();
	pre.ask(a,f1);reverse(a+1,a+n+1);suf.ask(a,f2);
//	for(int i=1;i<=n;i++)cout<<f1[i]<<" "<<f2[i]<<endl;
	for(int i=1;i<=n;i++)ans+=1ll*f1[i]*f2[n-i];
	cout<<ans;
	return 0;
}

CF710F String Set Queries

一个没见过的重构套路——二进制分组
每次加入一个字符串时都新建一棵大小为 1AC 自动机
然后用类似于 2048 一样的方式开始合并相同大小的自动机,那么最后留下的一定最多只有 log 个,而每个字符串最多经过 log 次重构
再来考虑删除的问题,发现维护的信息是具有可减性的,那么对于需要删除的串也建立一遍 AC 自动机然后减一减即可

但是实现起来会非常麻烦,最主要来源于平常的 AC 自动机默认根是零,而数组恰好也是零,而这里根不再是零时,数组相应地也要进行初始化的改变

遇到一个玄学问题,将 char 复制到 string 时会 RE,而如果采用初始化为空格之后采用 + 的方式来插入则不会有问题

update:现在来看一点儿也不玄学了好吧

代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e6+5;
int n,op,q[maxn];
char a[maxn];
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
struct{
	int len[maxn],trie[maxn][26],tot,cnt[maxn],nxt[maxn],tp,num;
	string b[maxn];
	struct Node{
		int l,r,siz,rt;
		Node(){}
		Node(int a,int b,int c,int d):l(a),r(b),siz(c),rt(d){}
	}sta[maxn];
	void insert(char* str,int rt){
		int p=rt,len1=strlen(str+1);
		for(int i=1;i<=len1;i++){
			int x=str[i]-'a';
//			cout<<"ppp  "<<x<<endl;
			if(trie[p][x]==rt){
				trie[p][x]=++tot;
				for(int j=0;j<26;j++)trie[tot][j]=rt;
			}
			p=trie[p][x];
		}
		cnt[p]++;
		return ;
	}
	void build(int rt){
		int hd=1,tl=0;
		nxt[rt]=rt;
		for(int i=0;i<26;i++)if(trie[rt][i]>rt)q[++tl]=trie[rt][i],nxt[trie[rt][i]]=rt;
		while(hd<=tl){
			int p=q[hd++];
			for(int i=0;i<26;i++){
				int x=trie[p][i];
				if(x==rt)trie[p][i]=trie[nxt[p]][i];
				else{
					nxt[x]=trie[nxt[p]][i];
					cnt[x]+=cnt[nxt[x]];
					q[++tl]=x;
				}
			}
		}
		return ;
	}
	void merge(){
//		cout<<"hhh"<<endl;
		sta[tp-1].siz<<=1;sta[tp-1].r=sta[tp].r;tp--;
		for(int i=sta[tp].rt;i<=tot;i++){
			for(int j=0;j<26;j++)trie[i][j]=0;
			nxt[i]=cnt[i]=0;
		}
		tot=sta[tp].rt;
		for(int i=0;i<26;i++)trie[tot][i]=tot;
		for(int i=sta[tp].l;i<=sta[tp].r;i++)insert(&b[i][0],sta[tp].rt);//,cout<<"do this "<<i<<endl;
		build(sta[tp].rt);
		return ;
	}
	void add(int i,char* a){
		sta[++tp]=Node(i,i,1,++tot);
//		cout<<tot<<" "<<sta[tp].rt<<endl;
		for(int i=0;i<26;i++)trie[tot][i]=tot;
		insert(a,tot);build(sta[tp].rt);
		while(tp>1&&sta[tp].siz==sta[tp-1].siz)merge();
		return ;
	}
	int doask(char* str,int j){
		int res=0,len1=strlen(str+1);
		for(int i=1;i<=len1;i++){
			int x=str[i]-'a';
			j=trie[j][x];res+=cnt[j];
//			cout<<j<<" ";
		}
		return res;
	}
	int ask(char* a){
		int res=0;
		for(int i=1;i<=tp;i++)res+=doask(a,sta[i].rt);
		return res;
	}
}_add,_del;
int main(){
	n=read();
	for(int i=1;i<=n;i++){
		op=read();
		scanf("%s",a+1);
		if(op==1){
			_add.len[++_add.num]=strlen(a+1);
			_add.b[_add.num]=" ";
			for(int i=1;i<=_add.len[_add.num];i++)_add.b[_add.num]+=a[i];
			_add.add(_add.num,a);
		}
		else if(op==2){
			_del.len[++_del.num]=strlen(a+1);
			_del.b[_del.num]=" ";
			for(int i=1;i<=_del.len[_del.num];i++)_del.b[_del.num]+=a[i];
			_del.add(_del.num,a);
		}
		else{
			printf("%d\n",_add.ask(a)-_del.ask(a));
			fflush(stdout);
		}
	}
	return 0;
}

Summary

  • AC 自动机能干得事无非就是多模匹配,直接往这方面想即可
  • 套路主要就是 dp 和树上问题,dp 经常容易漏掉
posted @   y_cx  阅读(214)  评论(3编辑  收藏  举报
编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示