trie树 & AC自动机 小结

trie树

trie 树,又称字典树或前缀树,是一种将多叉树与字符串结合起来的数据结构,通常用于字符串的高效存储与查询。其本质就是利用字符串之间的公共前缀,将重复的前缀合并在一起。
trie 树有三个基本性质:

  1. 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
  2. 根节点不包含字符,除根节点外每一个节点都只包含一个字符
  3. 每个节点的所有子节点包含的字符都不相同

我们可以据此来完成 trie 树的两个基本操作:存储与查询

存储

trie 树的存储过程,就是不断将字符串插入树中,从根节点开始从前往后依次遍历每一个字符,若不存在增加即可。
以插入字符串 him,her,cat,no,nova 为例,过程如下:

  1. 插入 him
  • 根节点不存在子节点 h,因此创建子节点 h
  • 在节点 h 的基础上插入第二个字符 i
  • 节点 h 不存在子节点 i,创建子节点 i
  • 在节点 i 的基础上插入第三个字符 m
  • 节点 i 不存在子节点 m,创建子节点 m。并将该节点标记为字符串结束标志,完成 him 字符串插入。
    image
  1. 插入 her
  • 根节点存在子节点 h。不用重新创建子节点 h
  • 在节点 h 的基础上插入第二个字符 e
  • 节点 h 不存在子节点 e,创建子节点 e
  • 在节点 e 的基础上插入第三个字符 r
  • 节点 e 不存在子节点 r,创建子节点 r。并将该节点标记为字符串结束标志,完成 her 字符串插入。
    image
  1. 插入 cat
  • 根节点不存在子节点 c,因此创建子节点 c
  • 在节点 c 的基础上插入第二个字符 a
  • 节点 c 不存在子节点 a,创建子节点 a
  • 在节点 a 的基础上插入第三个字符 t
  • 节点 a 不存在子节点 t,创建子节点 t。并将该节点标记为字符串结束标志,完成 cat 字符串插入。
    image
  1. 插入 no
  • 根节点不存在子节点 n,因此创建子节点 n
  • 在节点 n 的基础上插入第二个字符 o
  • 节点 n 不存在子节点 o,创建子节点 o。并将该节点标记为字符串结束标志,完成 no 字符串插入。
    image
  1. 插入 nova
  • 根节点存在子节点 n,不用重新创建子节点 n
  • 在节点 n 的基础上插入第二个字符 o
  • 节点 n 存在子节点 o,不用重新创建子节点 o
  • 在节点 o 的基础上插入第三个字符 v
  • 节点 o 不存在子节点 v,创建子节点 v
  • 在节点 v 的基础上插入第四个字符 a
  • 节点 v 不存在子节点 a,创建子节点 a。并将该节点标记为字符串结束标志,完成 nova 字符串插入。
    image

查询

trie 树的查询过程和插入的过程差不多,同样也是从根节点开始从前往后依次遍历每一个字符,直到找到目标节点为止。
以找 her 为例。

  • 从根节点遍历到 h
  • 从节点 h 遍历到节点 e
  • 从节点 e 遍历到节点 r ,查询完成。

例题

【luogu P8306】 【模板】字典树

字典树的模板题,照上面所说做即可,每次增加时给经过的节点的权值 +1 ,查询输出遍历到节点的权值即可。

Code

#include<bits/stdc++.h>
using namespace std;
long long a[3000005][66],n,m,cnt,f[3000005];//定义时不用邻接链表,开二维数组存储
long long turn(char x)//将每个字母或数字转化成对应编码
{
	if(x<='9')return x-'0'+1;
	else if(x<='Z')return x-'A'+12;
	return x-'a'+38; 
}
void make(string x)//存储字符串
{
	long long p=1;//从根节点开始遍历
	for(int i=0;i<x.size();i++)
	{
		if(a[p][turn(x[i])])p=a[p][turn(x[i])];//若存在直接往下查
		else a[p][turn(x[i])]=++cnt,p=cnt;//否则新建节点
		f[p]++;//权值+1
	}
	return;
}
long long search(string x)//查询
{
	long long p=1;//从根节点开始
	for(int i=0;i<x.size();i++)
	{
		if(a[p][turn(x[i])])p=a[p][turn(x[i])];//往下查
		else return 0;//若没有直接返回0
	}
	return f[p];//返回当前找到节点权值
}
void trie()
{
	string x;
	cnt=1;
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>x;
		make(x);
	}
	for(int i=1;i<=m;i++)
	{
		cin>>x;
		cout<<search(x)<<'\n';
	}
	for(int i=0;i<=cnt+1;i++)
	{
		f[i]=0;
		for(int j=0;j<=65;j++)a[i][j]=0;
	}//清空数组
	return;
}
int main()
{
	long long qwe;
	scanf("%lld",&qwe);
	for(int i=1;i<=qwe;i++)trie();








  return 0;
}

【luogu P6924】 「EZEC-4」可乐

trie 树的一个经典应用。
将每个数转化成二进制,当作字符串存储进 trie 树中,这样,trie 树便呈现一个二叉树的结构。
查询时从高位到低位查,若 k1 ,则异或后该位相同且前面满足要求的树可取,不同的数继续往下找。
k0 时,也是继续往下找。

Code

#include<bits/stdc++.h>
using namespace std;
long long n,m,f[2000005],num=1;
long long a[2000005][2];
void dijah(long long x)//存储部分
{
	long long p=1;
	for(int i=21;i>=0;i--)
	{
		if(x&(1<<i))
		{
			if(a[p][1])p=a[p][1];
			else a[p][1]=++num,p=num;
		}
		else
		{
			if(a[p][0])p=a[p][0];
			else a[p][0]=++num,p=num;
		}
		f[p]++;
	}
	return;
}
long long gaia(long long p,long long x)//代表现在找到p节点,找到第x位的结果
{
	if(x==0)return f[p];
	if(m&(1<<x))return max(gaia(a[p][0],x-1)+f[a[p][1]],gaia(a[p][1],x-1)+f[a[p][0]]);//若k该位为1,那么不同任取,相同继续找,取x该位为0或1时答案的最大值即可
	return max(gaia(a[p][0],x-1),gaia(a[p][1],x-1));//若该位为0,继续找
}
int main()
{
	long long x;
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++)
	{
		scanf("%lld",&x);
		dijah(x);
	}
	cout<<gaia(1,21);//答案即为从根节点出发,找第21位逐步往后找








  return 0;
}

AC自动机

AC自动机是一种多模式匹配算法,将 KMP 算法与 trie 树结合起来,与 KMP 类似,AC自动机也是用来处理字符串匹配的问题。与 KMP 不同的是,KMP 用来处理单模式串问题,即问模式串 T 是否是主串 S 的字串,而AC自动机则能处理多模式串的问题。
AC自动机的构造过程分为 3 步:

  1. 建立模式串的字典树
  2. 添加失配路径
  3. 搜索待处理的文本

建立模式串的字典树

跟前面 trie 树构建过程一样,将多个模式串建成一棵 trie 树。

添加失配路径

这个步骤是AC自动机中最重要的一步。
添加失配路径即为构建 fail 指针,与 KMP 含义相同,都为指向当前节点所代表的的字符串的最长后缀,搜索时若匹配失败可以直接跳转。
fail 指针构建过程如下

  • 根节点的子节点的 fail 指针都指向根节点,将根节点空的儿子指向自己
  • 广度优先搜索,遍历到某个节点时,它的 fail 指针所指即为父亲的 fail 指针所指节点的相同字母儿子
  • 若该节点不存在,继续跳 fail 指针即可

举个例子:
say,she,shr,her 已经构建好 trie 树,要添加失配路径。
先将根节点的子节点的 fail 指针指向 root
image
广度优先搜索到 a,h,e , a 的父亲的 fail 指针所指的节点为 rootroota 儿子为 rootafail 指针指向 root
h 的父亲的 fail 指针所知的节点的 h 儿子存在, hfail 指向该节点。
e 节点与 a 同理,指向 root 节点。
image
遍历到第 3 层,有节点 y,e,r,ry 节点的父亲的 fail 指针的 y 儿子为 rootyfail 指针为 root
e 节点父亲的 fail 指针指向节点有 e 儿子,e 节点 fail 指针指向该节点。
r,r 节点与 y 节点同理,指向 root 节点。
image
这样 fail 指针便构建好了。

搜索待处理的文本

从根节点开始往后遍历每个字符,每个字符都跳一遍 fail 指针查询答案,若走不了时就跳 fail 指针直到遍历完字符串。

例题

【luogu P3796】 AC 自动机(简单版 II)

AC自动机的模板题。
构建 trie 树时在字符串结尾节点的权值打标记,查询跳 fail 指针时记录每个有权值的节点的出现次数,取最大即可。

Code

#include<bits/stdc++.h>
using namespace std;
long long num,a[10505][27],co,d[155],s,v[10505],fail[10505];
queue<long long> l;
string b[155];
void dijah(string x)//构建字典树
{
	long long p=1;
	for(int i=0;i<x.size();i++)
	{
		if(a[p][x[i]-'a'])p=a[p][x[i]-'a'];
		else a[p][x[i]-'a']=++num,p=num;
	}
	v[p]=++co;//标记
	return;
}
void build()
{
	for(int i=0;i<26;i++)//遍历root节点的子节点
	{
		if(a[1][i])
		{
			fail[a[1][i]]=1;//root节点的子节点的fail指针指向root节点
			l.push(a[1][i]);//加入队列准备bfs
		}
		else a[1][i]=1;//不存在指向root节点
	}
	long long x;
	while(l.size())//bfs
	{
		x=l.front();
		l.pop();
		for(int i=0;i<26;i++)//处理出每个儿子的fail指针
		{
			if(a[x][i])
			{
				fail[a[x][i]]=a[fail[x]][i];//该节点的fail指针的相同儿子即为该节点相同儿子的fail指针所指
				l.push(a[x][i]);//准备bfs
			}
			else a[x][i]=a[fail[x]][i];//优化,下一次有别的节点跳fail跳到该节点时可以快速转移
		}
	}
	return;
}
void gaia(string x)
{
	long long p=1;
	for(int i=0;i<x.size();i++)
	{
		if(a[p][x[i]-'a'])p=a[p][x[i]-'a'];//依次往下找
		for(int j=p;j;j=fail[j])//跳fail找答案
		{	
			d[v[j]]++;//统计次数
			if(v[j]!=0)s=max(s,d[v[j]]);//取最大
		}
	}
	return;
}
void dijah(long long n)
{
	string x;
	num=1;
	co=s=0;
	for(int i=1;i<=n;i++)
	{
		cin>>b[i];
		dijah(b[i]);
	}
	build();
	cin>>x;
	gaia(x);
	printf("%lld\n",s);
	for(int i=1;i<=co;i++)
	{
		if(d[i]==s)cout<<b[i]<<'\n',d[i]=-1;
	}
	for(int i=1;i<=num;i++)
	{
		v[i]=fail[i]=0;
		for(int j=0;j<26;j++)a[i][j]=0;
	}
	for(int i=0;i<=co;i++)d[i]=0;
	return;
}
int main()
{
	long long n;
	while(cin>>n)
	{
		if(n==0)break;
		dijah(n);
	}








  return 0;
}

【luogu P3041】 [USACO12JAN] Video Game G

AC自动机+DP
处理出 fail 指针后开始 DP ,每一次将某个节点的答案推进到子节点,并将作为模式串结尾的节点的贡献 +1

Code

#include<bits/stdc++.h>
using namespace std;
long long n,m,a[305][3],num=1,f[305],b[305],v[305],fail[305];
void dijah(string x)
{
	long long p=1;
	for(int i=0;i<x.size();i++)
	{
		if(a[p][x[i]-'A'])p=a[p][x[i]-'A'];
		else a[p][x[i]-'A']=++num,p=num;
	}
	v[p]++;
	return;
}
queue<long long> l;
void build()
{
	long long x;
	for(int i=0;i<3;i++)
	{
		if(a[1][i])
		{
			fail[a[1][i]]=1;
			l.push(a[1][i]); 
		}
		else a[1][i]=1;
	}
	while(l.size())
	{
		x=l.front();
		l.pop();
		for(int i=0;i<3;i++)
		{
			if(a[x][i])
			{
				fail[a[x][i]]=a[fail[x]][i];
				l.push(a[x][i]);
			}
			else a[x][i]=a[fail[x]][i];
		}
	}
	return;
}
int main()
{
	string x;
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++)
	{
		cin>>x;
		dijah(x);	
 	}
 	build();
 	memset(f,-2,sizeof(f));
 	f[1]=0;
 	for(int i=1;i<=m;i++)//DP
 	{
 		memset(b,-2,sizeof(b));
 		for(int j=1;j<=num;j++)
 		{
 			for(int u=0;u<3;u++)
 			{
 				b[a[j][u]]=max(b[a[j][u]],f[j]);//推进到子节点
			}
		}
		for(int j=1;j<=num;j++)
		{
			f[j]=b[j];//滚动数组
			for(int u=j;u!=1;u=fail[u])f[j]+=v[u];//将结尾有模式串的字符串的贡献+1
		}
	}
	long long s=0;
	for(int i=1;i<=num;i++)s=max(s,f[i]);
	cout<<s;







  return 0;
}

【luogu P3121】 [USACO15FEB] Censoring G

开一个栈存储遍历时到过的位置。
可以删除是,将栈顶的元素删除等量多个,退回的将删除的字符串的前面一个节点继续做AC自动机。

Code

#include<bits/stdc++.h>
using namespace std;
struct datay
{
	long long l,r;
}d[100005];
long long n,num=1,fail[100005],a[100005][26],v[100005],deep[100005],co;
set<long long> v1;
void dijah(string x)
{
	long long p=1;
	for(int i=0;i<x.size();i++)
	{
		if(a[p][x[i]-'a'])p=a[p][x[i]-'a'];
		else a[p][x[i]-'a']=++num,p=num;
	}
	v[p]=x.size();
	return;
}
queue<long long> l;
void build()
{
	for(int i=0;i<26;i++)
	{
		if(a[1][i])
		{
			deep[a[1][i]]=1;
			l.push(a[1][i]);
			fail[a[1][i]]=1;
		}
		else a[1][i]=1;
	}
	long long x;
	while(l.size())
	{
		x=l.front();
		l.pop();
		for(int i=0;i<26;i++)
		{
			if(a[x][i])
			{
				deep[a[x][i]]=deep[x]+1;
				fail[a[x][i]]=a[fail[x]][i];
				l.push(a[x][i]);
			}
			else a[x][i]=a[fail[x]][i];
		}
	}
	return;
}
stack<long long> re;
void gaia(string x)
{
	long long p=1,q,t=-1;
	re.push(1);
	for(int i=0;i<x.size();i++)
	{
		p=a[p][x[i]-'a'];
		re.push(p);
		if(v[p])
		{
			q=p;
			for(int j=1;j<=v[p];j++)re.pop(); 
			p=re.top();//退回到某个节点
			d[++co].l=v[q];
			d[co].r=i;//存储删去的范围
		}
	}
	return;
}
int main()
{
	string x,y;
	cin>>x;
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)
	{
		cin>>y;
		dijah(y);
	}
	build();
	gaia(x);
	for(int i=0;i<x.size();i++)v1.insert(i);
	set<long long>::iterator q,w;
	for(int i=1;i<=co;i++)
	{
		q=v1.lower_bound(d[i].r);
		for(int j=1;j<=d[i].l;j++)
		{
			w=q;
			w--;
			v1.erase(q);
			q=w;
		}
	}//用set找未被删的位
	q=v1.begin();
	for(;q!=v1.end();q++)
	{
		cout<<x[*q];
	}









  return 0;
}

【luogu P2444】 [POI2000] 病毒

即使找存不存在一个环不经过任何一个作为字符串结尾的节点,暴力 dfs 即可。

Code

#include<bits/stdc++.h>
using namespace std;
long long n,a[30005][2],num=1,v[30005],fail[30005],b[30005],f[30005];
bool qwe=false;
void dijah(string x)
{
	long long p=1;
	for(int i=0;i<x.size();i++)
	{
		if(a[p][x[i]-'0'])p=a[p][x[i]-'0'];
		else a[p][x[i]-'0']=++num,p=num;
	}
	v[p]++;
	return;
}
queue<long long> l;
void build()
{
	for(int i=0;i<2;i++)
	{
		if(a[1][i])
		{
			fail[a[1][i]]=1;
			l.push(a[1][i]);
		}
		else a[1][i]=1;
	}
	long long x;
	while(l.size())
	{
		x=l.front();
		l.pop();
		for(int i=0;i<2;i++)
		{
			if(a[x][i])
			{
				fail[a[x][i]]=a[fail[x]][i];
				l.push(a[x][i]);
			}
			else a[x][i]=a[fail[x]][i];
		}
	}
	return;
}
void dijah(long long x)
{
	for(int j=x;j!=1;j=fail[j])
	{
		if(v[j])return;//若作为结尾直接返回
	}
	if(b[x])//若以遍历过则成功找到环,输出TAK
	{
		qwe=true;
		return;
	}
	b[x]=1;
	if(!f[a[x][0]])dijah(a[x][0]);//往下找
	if(!f[a[x][1]])dijah(a[x][1]);
	b[x]=0;
	f[x]=1;
	return;
} 
int main()
{
	string x;
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)
	{
		cin>>x;
		dijah(x);
	}
	build();
	dijah(1);
	if(!qwe)printf("NIE\n");
	else printf("TAK\n");








  return 0;
}

【luogu P2414】 [NOI2011] 阿狸的打字机

AC自动机fail指针所指能够成一个树为 fail 树,每一次跳 fail 指针查询就是在树上往根节点跳。
用树状数组单点修改与 dfn 序区间查询加离线算法即可。

Code

#include<bits/stdc++.h>
using namespace std;
struct datay
{
	int x,y,z,v;
}que[100005];
int a[100005][26],m,fa[100005],num,fail[100005],f[100005],n,dfn[100005],out[100005],q=1,g,num1,last[100005];
vector<int> t[100005];
string p;
bool cmp1(datay q,datay w)
{
	return q.y<w.y;
}
queue<int> l;
bool cmp2(datay q,datay w)
{
	return q.z<w.z;
}
int lowbit(int x)
{
	return x&(-x);
}
void dijah(int x,int y)
{
//	cout<<x<<'\n';
	for(int i=x;i<=num;i+=lowbit(i))f[i]+=y;
	return;
}
int gaia(int x)
{
	int h=0;
	while(x)
	{
		h+=f[x];
		x-=lowbit(x);
	}
	return h;
}
void build()
{
	for(int i=0;i<26;i++)
	{
		if(a[1][i])
		{
			l.push(a[1][i]);
			fail[a[1][i]]=1;
		}
		else a[1][i]=1;
	}
	int x;
	while(l.size())
	{
		x=l.front();
		l.pop();
		for(int i=0;i<26;i++)
		{
			if(a[x][i])
			{
				l.push(a[x][i]);
				fail[a[x][i]]=a[fail[x]][i];
			}
			else a[x][i]=a[fail[x]][i];
		}
	}
	return;
}
void dfs(int x)
{
	dfn[x]=++num1;
	for(int i=0;i<t[x].size();i++)
	{
		dfs(t[x][i]);
	}
	out[x]=num1; 
	return;
}
void modify()
{
	for(;g<p.size();g++)
	{
//		cout<<g<<' '<<q<<'\n';
		if(p[g]=='B')dijah(dfn[q],-1),q=fa[q];
		else if(p[g]=='P')
		{
			g++;
			return;
		}
		else 
		{
			q=a[q][p[g]-'a'];
			dijah(dfn[q],1);
		}
	}
	return;
}
int main()
{
	cin>>p;
	scanf("%d",&m);
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&que[i].x,&que[i].y);
		que[i].z=i;
	}
	sort(que+1,que+m+1,cmp1);
	num=1;
	q=1;
	for(int i=0;i<p.size();i++)
	{
		if(p[i]=='B')q=fa[q];
		else if(p[i]=='P')last[++n]=q;
		else
		{
			if(a[q][p[i]-'a'])q=a[q][p[i]-'a'];
			else a[q][p[i]-'a']=++num,fa[num]=q,q=num;
		}
	}
	build();
	for(int i=2;i<=num;i++)
	{
		t[fail[i]].push_back(i);
	}
//	cout<<"doge\n";
	q=1;
	dfs(1);
//	for(int i=1;i<=num;i++)cout<<fa[i]<<' '<<fail[i]<<' '<<dfn[i]<<' '<<out[i]<<'\n';
	g=0;
	for(int i=1;i<=m;i++)
	{
		if(que[i].y!=que[i-1].y)
		{
			for(int j=que[i-1].y+1;j<=que[i].y;j++)modify();
		}
		que[i].v=gaia(out[last[que[i].x]])-gaia(dfn[last[que[i].x]]-1);
	}
	sort(que+1,que+m+1,cmp2);
	for(int i=1;i<=m;i++)printf("%d\n",que[i].v);
	








  return 0;
}

【CF1202E】 You Are Given Some Strings...

先正着坐一遍AC自动机,记录到每个点的字符串个数,再把所有字符串反过来再做一遍,两边相乘后累加即可。

Code

#include<bits/stdc++.h>
using namespace std;
string p,t[400005];
long long f1[400005],n,a[400005][26],num=1,v[400005],fail[400005],f2[400005],s=0;
queue<long long> l;
void dijah(string x)
{
	long long q=1;
	for(int i=0;i<x.size();i++)
	{
		if(a[q][x[i]-'a'])q=a[q][x[i]-'a'];
		else a[q][x[i]-'a']=++num,q=num;
	}
	v[q]++;
	return;
}
void build()
{
	long long x;
	for(int i=0;i<26;i++)
	{
		if(a[1][i])
		{
			l.push(a[1][i]);
			fail[a[1][i]]=1;
			v[a[1][i]]+=v[1];
		}
		else a[1][i]=1;
	}
	while(l.size())
	{
		x=l.front();
		l.pop();
		for(int i=0;i<26;i++)
		{
			if(a[x][i])
			{
				l.push(a[x][i]);
				fail[a[x][i]]=a[fail[x]][i];
				v[a[x][i]]+=v[fail[a[x][i]]];
			}
			else a[x][i]=a[fail[x]][i];
		}
	}
	return;
}
void gaia1()
{
	long long q=1;
	for(int i=0;i<p.size();i++)
	{
		q=a[q][p[i]-'a'];
		f1[i]=v[q];
	}
	return;
}
void gaia2()
{
	long long q=1;
	for(int i=0;i<p.size();i++)
	{
		q=a[q][p[i]-'a'];
		f2[i]=v[q];
	}
	return;
}
string h="";
string re(string x)
{
	h="";
	for(int i=x.size()-1;i>=0;i--)h+=x[i];
	return h;
}
int main()
{
	cin>>p;
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)cin>>t[i],dijah(t[i]);
	build();
	gaia1();
	memset(a,0,sizeof(a));
	memset(v,0,sizeof(v));
	memset(fail,0,sizeof(fail));
	while(l.size())l.pop();
	num=1;
	p=re(p);
	for(int i=1;i<=n;i++)t[i]=re(t[i]),dijah(t[i]);
	build();
	gaia2();
	for(int i=0;i<p.size()-1;i++)s+=f1[i]*f2[p.size()-i-2];
	cout<<s;
	
	








  return 0;
}

【luogu P2292】 [HNOI2004] L 语言

由于 |s| 很小,构建出AC自动机后,每个节点可以记录出以它为结尾可以有哪些长度的字符串。
那么在AC自动机匹配时,可以记录当前字符的前 1~20 个字符是否已被理解,再结合当前节点在AC自动机上的记录更新答案。
由于 1|s|20 ,直接状压

Code

#include<bits/stdc++.h>
using namespace std;
int n,m,num=1,fail[1005],a[1005][36],v[1005],f[1005];
//vector<int> v[1005];
void dijah(string x)
{
	int p=1;
	for(int i=0;i<x.size();i++)
	{
		if(a[p][x[i]-'a'])p=a[p][x[i]-'a'];
		else a[p][x[i]-'a']=++num,p=num;
	}
	v[p]=x.size();
	return;
}
queue<int> l;
void build()
{
	int x;
	for(int i=0;i<26;i++)
	{
		if(a[1][i])
		{
			l.push(a[1][i]);
			fail[a[1][i]]=1;
		}
		else a[1][i]=1;
	}
	while(l.size())
	{
		x=l.front();
		l.pop();
		for(int i=0;i<26;i++)
		{
			if(a[x][i])
			{
				l.push(a[x][i]);
				fail[a[x][i]]=a[fail[x]][i]; 
			}
			else a[x][i]=a[fail[x]][i];
		}
	}
	return;
}
int main()
{
	string x;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		cin>>x;
		dijah(x);
	}
	build();
	for(int i=2;i<=num;i++)
	{
		for(int j=i;j!=1;j=fail[j])
		{
			if(v[j])f[i]+=(1<<(v[j]));
		}
//		cout<<i<<':'<<fail[i]<<' '<<f[i]<<'\n'; 
	}
	int q=(1<<21);
//	for(int )
	for(int i=1;i<=m;i++)
	{
		x="";
		char c=getchar();
		while(c<'a'||c>'z')c=getchar();
		while(c>='a'&&c<='z')x+=c,c=getchar();
//		x=read();
		int p=1,s=2,ans=-1;
		for(int j=0;j<x.size();j++)
		{
			p=a[p][x[j]-'a'];
			if(s&f[p])s++,ans=j;
//			cout<<j<<' '<<s<<' '<<p<<' '<<f[p]<<'\n';
			s<<=1;
			if(s&q)s^=q;
		}
		printf("%d\n",ans+1);
	}








  return 0;
}

【CF86C】 Genetic engineering

AC自动机+DP
fi,j,u 为当前做到第 i 个字符,trie 树上第 j 个节点,从当前节点往前数 u 个字符未被覆盖。
若当前节点有作为字符串结尾且长度大于 u ,那么 u 必须变为 0
否则向下一位推进。

Code

#include<bits/stdc++.h>
using namespace std;
const long long mod=1e9+9;
int n,m,a[105][4],num=1,fail[105];
long long v[105];
long long f[1005][105][12];
queue<int> l;
//vector<int> t[105];
void build()
{
	int x;
	for(int i=0;i<4;i++)
	{
		if(a[1][i])
		{
			l.push(a[1][i]);
			fail[a[1][i]]=1;
		 } 
		 else a[1][i]=1;
	}
	while(l.size())
	{
		x=l.front();
		l.pop();
		for(int i=0;i<4;i++)
		{
			if(a[x][i])
			{
				l.push(a[x][i]);
				fail[a[x][i]]=a[fail[x]][i];
				v[a[x][i]]=max(v[a[x][i]],v[fail[a[x][i]]]);
			}
			else a[x][i]=a[fail[x]][i];
		}
	}
	return;
}
int main()
{
//	memset(v,-1,sizeof(v));
	int p=1;
	long long s=0;
	char c;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
		s=0;
		p=1;
		c=getchar();
		while(c<'A'||c>'Z')c=getchar();
		while(c>='A'&&c<='Z')
		{
			s++;
			if(c=='A')c='a';
			else if(c=='C')c='b';
			else if(c=='G')c='c';
			else c='d';
			if(a[p][c-'a'])p=a[p][c-'a'];
			else a[p][c-'a']=++num,p=num;
			c=getchar();
		}
		v[p]=max(v[p],s);
	}
	build();
//	for(int i=1;i<=num;i++)cout<<i<<' '<<fail[i]<<' '<<v[i]<<'\n';
//	memset(f,1,sizeof(f));
	f[0][1][0]=1;
//	for(int i=1;i<=num;i++)
//	{
//		for(int j=0;j<4;j++)
//		{
//			if(!b[i][a[i][j]])
//			{
//				b[i][a[i][j]]=1;
//				t[i].push_back(a[i][j]);
//			}
//		}
//	}
	for(int i=0;i<=n;i++)
	{
		for(int j=1;j<=num;j++)
		{
			for(int u=0;u<=10;u++)
			{
				for(int k=0;k<4;k++)
				{
//					if(t[j][k]==1)continue;
					if(v[a[j][k]]>u)f[i+1][a[j][k]][0]=(f[i+1][a[j][k]][0]+f[i][j][u])%mod;
					else f[i+1][a[j][k]][u+1]=(f[i+1][a[j][k]][u+1]+f[i][j][u])%mod;
//					if(i==0&&j==1&&u==0&&a[j][k]==2)cout<<f[i+1][a[j][k]][0]<<'\n';
				}
//				for(int x1=j;x1!=1;x1=fail[x1])
//				{
//					if(v[x1])
//				}
			}
		}
	}
	s=0;
	for(int i=1;i<=num;i++)s=(s+f[n][i][0])%mod;
	cout<<s;








  return 0;
}

【luogu P5840】 [COCI2015] Divljak

很明显,建好AC自动机,构建 fail 树后用树状数组+dfn 序维护即可。
问题是怎么去重。
我们可以每次增加字符串时,把它经过的点给求出来,求路径并即可。

Code

#include<bits/stdc++.h>
using namespace std;
int n,num=1,a[2000005][26],v[2000005],m,fail[2000005],f[2000005][22],deep[2000005],dfn[2000005],out[2000005],num1,f1[2000005],d[2000005],tr;
queue<int> l;
vector<int> t[2000005];
int lowbit(int x)
{
	return x&(-x);
}
void dijah(int x,int y)
{
//	cout<<x<<' '<<y<<'\n';
	if(x==0)return;
	for(int i=x;i<=num;i+=lowbit(i))f1[i]+=y;
	return;
}
int gaia(int x)
{
	int h=0;
	while(x)
	{
		h+=f1[x];
		x-=lowbit(x);
	}
	return h;
}
void build()
{
	for(int i=0;i<26;i++)
	{
		if(a[1][i])
		{
			l.push(a[1][i]);
			fail[a[1][i]]=1;
		}
		else a[1][i]=1;
	}
	int x;
	while(l.size())
	{
		x=l.front();
		l.pop();
		for(int i=0;i<26;i++)
		{
			if(a[x][i])
			{
				l.push(a[x][i]);
				fail[a[x][i]]=a[fail[x]][i];
			}
			else a[x][i]=a[fail[x]][i];
		}
	}
	return;
}
void dfs(int x,int y)
{
	deep[x]=deep[y]+1;
	f[x][0]=y;
	dfn[x]=++num1;
	for(int i=0;i<t[x].size();i++)
	{
		dfs(t[x][i],x);
	}
	out[x]=num1;
	return;
}
int LCA(int x,int y)
{
	if(deep[x]<deep[y])swap(x,y);
	for(int i=21;i>=0;i--)
	{
		if(deep[f[x][i]]>=deep[y])x=f[x][i];
	}
	if(x==y)return x;
	for(int i=21;i>=0;i--)
	{
		if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
	}
	return f[x][0];
}
bool cmp(int q,int w)
{
	return dfn[q]<dfn[w];
}
int main()
{
	int p=1,x,y;
	scanf("%d",&n);
	char c=getchar();
	for(int i=1;i<=n;i++)
	{
		p=1;
		while(c<'a'||c>'z')c=getchar();
		while(c>='a'&&c<='z')
		{
			if(a[p][c-'a'])p=a[p][c-'a'];
			else a[p][c-'a']=++num,p=num;
			c=getchar();
		}
		v[i]=p;
	}
	build();
	for(int i=2;i<=num;i++)t[fail[i]].push_back(i);
	dfs(1,0);
	for(int i=1;i<=21;i++)
	{
		for(int j=1;j<=num;j++)f[j][i]=f[f[j][i-1]][i-1];
	}
//	for(int i=1;i<=num;i++)
//	{
//		printf("%d:%d %d %d\n",i,fail[i],dfn[i],out[i]);
//	}
	scanf("%d",&m);
	for(int qw=1;qw<=m;qw++)
	{
		scanf("%d",&p);
		if(p==1)
		{
			tr=0;
			p=1;
			while(c<'a'||c>'z')c=getchar();
			while(c>='a'&&c<='z')
			{
//				cout<<"doge\n";
				p=a[p][c-'a'];
				d[++tr]=p;
				c=getchar();
			}
			sort(d+1,d+tr+1,cmp);
//			for(int i=1;i<=tr;i++)cout<<d[i]<<' ';
//			cout<<'\n';
			for(int i=1;i<=tr;i++)
			{
				dijah(dfn[d[i]],1);
				if(i>1)dijah(dfn[LCA(d[i-1],d[i])],-1);
//				if(df)
//				if(i>1&&out[dfn[LCA(d[i-1],d[i])]]<dfn[d[i]])cout<<LCA(d[i-1],d[i])<<' '<<d[i-1]<<' '<<d[i]<<'\n';
//				if(i>1&&dfn[LCA(d[i-1],d[i])]>dfn[d[i]])cout<<"doge\n";
			}
		}
		else
		{
			scanf("%d",&x);
			printf("%d\n",gaia(out[v[x]])-gaia(dfn[v[x]]-1));
		}
	}








  return 0;
}
/*
5
abc
ab
cab
bc
abd
3132
1 aacbabcbbc
*/
posted @   dijah  阅读(60)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
点击右上角即可分享
微信分享提示