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\) 树便呈现一个二叉树的结构。
查询时从高位到低位查,若 \(k\)\(1\) ,则异或后该位相同且前面满足要求的树可取,不同的数继续往下找。
\(k\)\(0\) 时,也是继续往下找。

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\) 指针所指的节点为 \(root\)\(root\)\(a\) 儿子为 \(root\)\(a\)\(fail\) 指针指向 \(root\)
\(h\) 的父亲的 \(fail\) 指针所知的节点的 \(h\) 儿子存在, \(h\)\(fail\) 指向该节点。
\(e\) 节点与 \(a\) 同理,指向 \(root\) 节点。
image
遍历到第 \(3\) 层,有节点 \(y,e,r,r\)\(y\) 节点的父亲的 \(fail\) 指针的 \(y\) 儿子为 \(root\)\(y\)\(fail\) 指针为 \(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\le |s| \le 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
\(f_{i,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 @ 2024-02-13 15:53  dijah  阅读(25)  评论(0编辑  收藏  举报