「刷题笔记」AC自动机

自动AC机

Keywords Research

板子题,同luoguP3808,不过是多测。
然后多测不清空,(MLE)两行泪。
板子放一下

#include<bits/stdc++.h>
using namespace std;
#define ll int
#define ull unsigned long long
#define ZZ_zuozhe int main()
#define S 1000010

ll n;
char tmp[S];
ll vcn=0;
struct vertex
{
	ll fail;
	ll son[30];
	ll num;
}tree[S];

ll q[S],l,r;

void build()
{
	ll len=strlen(tmp);
	ll rt=0;
	for(int i=0;i<len;i++)
	{
		ll t=tmp[i]-'a';
		if(!tree[rt].son[t])tree[rt].son[t]=++vcn;
		rt=tree[rt].son[t];
	}
	tree[rt].num++;
}

void getfail()
{
	l=r=0;
	tree[0].fail=0;
	for(int i=0;i<26;i++)
	{
		if(tree[0].son[i])
		{
			tree[tree[0].son[i]].fail=0;
			q[++r]=tree[0].son[i];
		}
	}
	while(l<r)
	{
		ll u=q[++l],v;
		for(int i=0;i<26;i++)
		{
			v=tree[u].son[i];
			if(v)
			{
				tree[v].fail=tree[tree[u].fail].son[i]; 
				q[++r]=v;
			}
			else tree[u].son[i]=tree[tree[u].fail].son[i];	
		}
	}
}

ll AC()
{
	ll len=strlen(tmp);
	ll u=0,ans=0,v;
	for(int i=0;i<len;i++)
	{
		ll t=tmp[i]-'a';
		u=tree[u].son[t];
		v=u;
		while(v&&tree[v].num!=-1)
		{
			ans+=tree[v].num;
			tree[v].num=-1;
			v=tree[v].fail;
		}
	}
	return ans;
}

void clear()
{
	memset(tree,0,sizeof tree);
	vcn=0;
}

ll T;

ZZ_zuozhe
{
	scanf("%d",&T);
	while(T--)
	{
		clear();
		scanf("%d",&n);
		while(n--)
		{
			scanf("%s",tmp);
			build();
		}
		getfail();
		scanf("%s",tmp);
		printf("%d
",AC());
	}
	return 0;
}

玄武密码

求最大匹配前缀。多模式串。
似乎是板子……(然而当时并没打熟
跳fail的时候需要把能匹配的位置都标上,能匹配上的肯定是前缀。
然后再扫一下(trie),找之前的标记,并取最大下标。
求最大匹配后缀的话是不是要倒一下……一会问问
其实只有四个字母应该压下空间的,不过这题不压也能过(懒 ZZ 懒)
然后就是以后要看清题里给的是小写字母还是大写的,要不数组会越界于无形之中(如果数据正好又比较小,(wa)都不知道怎么(wa)的……

TJOI2013 单词

文章单词之间是有空格的,分开插入就行,刚开始想多了……
(哪有英语文章不写空格的啊喂
(fail)有一个性质就是,一个串的(fail)其实是这个串的一个后缀,也就是这个串包含了他的(fail)
所以说,一个单词是多少单词的(fail),他就出现了多少次
考虑一个(fail)指针组成的(fail)树,那么一个单词出现次数就是(fail)树中以他为根的子树大小
刚开始以为是要在(trie)上做一个(dfs),但是这样统计实际上是不完全的,例如:

abc
babc

这时只考虑了一个串是另外一个串的前缀的情况。
核心代码长这样:

for(int i=vcn;i>=1;i--)tree[tree[q[i]].fail].num+=tree[q[i]].num;
for(int i=0;i<n;i++)printf("%lld
",tree[tail[i]].num);

POI2000 病毒

看题意很迷惑,然后画了点图,发现大概是要在(trie)上找到一个不经过串尾的环
不经过串尾保证他不会和某个模式串匹配,环保证他能无限循环
然后(dfs)就行了
注意:一个结点不断跳(fail),肯定是能跳到(0)的,但是需要保证这一路上没有危险节点,所以标记危险节点的时候,除了标模式串尾的结点,不如把在多次跳(fail)后会到达危险节点的结点也标上。
然后dfs时跳的是两个儿子,而没有判(0),这是因为之前在(getfail)的时候已经把这样不存在的儿子引到父亲的(fail)的儿子了,所以其实没有影响的。
代码或许需要存一下……

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define ZZ_zuozhe int main()
#define S 60005

ll n;
struct vertex
{
	ll fail;
	ll son[2];
}tree[S];
ll dg[S];
ll q[S];
ll vcn=0;
char tmp[S];

void ins()
{
	ll rt=0;
	ll len=strlen(tmp);
	for(int i=0;i<len;i++)
	{
		if(!tree[rt].son[tmp[i]-'0'])tree[rt].son[tmp[i]-'0']=++vcn;
		rt=tree[rt].son[tmp[i]-'0'];
	}
	dg[rt]=1;
}

void getfail()
{
	ll l,r;
	l=r=0;
	tree[0].fail=0;
	for(int i=0;i<2;i++)
	{
		if(tree[0].son[i])
		{
			tree[tree[0].son[i]].fail=0;
			q[++r]=tree[0].son[i];
		}
	}
	while(l<r)
	{
		ll u=q[++l],v;
		for(int i=0;i<2;i++)
		{
			v=tree[u].son[i];
			if(v)
			{
				tree[v].fail=tree[tree[u].fail].son[i];
				dg[v]|=dg[tree[v].fail];
				q[++r]=v;
			}
			else tree[u].son[i]=tree[tree[u].fail].son[i];
		}
	}
}



bool vis[S]={0},fvis[S]={0};
bool dfs(ll rt)
{
	if(vis[rt])return 1;
	if(dg[rt]||fvis[rt])return 0;	
	vis[rt]=fvis[rt]=1;
	bool res=0;
	if(!dg[tree[rt].son[0]])res|=dfs(tree[rt].son[0]);
	if(!dg[tree[rt].son[1]])res|=dfs(tree[rt].son[1]);
	//if(!dg[tree[rt].fail])res|=dfs(tree[rt].fail);
	vis[rt]=0;
	return res;
}

ZZ_zuozhe
{
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%s",tmp);
		ins();
	}
	getfail();
	if(dfs(0))puts("TAK");
	else puts("NIE");
	return 0;
}

最短母串

警醒:把一个数类型设成(char),白调(2h)

一点点dp(其实主要是状压),但我可以送命了qaq
trie树先建出来
(n)的范围不是很大,所以可以考虑用状压记录各个字符串是否被用过,至于状态,因为一个字符串用过肯定经过了他的结尾那个结点,所以每个结点记录上状态,在建树的时候结尾要附上这个字符串的状态((1<<x))
根据fail的性质,getfail时,结点和他的fail的状态要合并,因为到了这个节点就是可以选择跳fail的,就相当于已经走了之后的路了
为什么此处必定跳fail呢?因为这里如果有fail就说明这个节点的后面可以匹配了,这个时候不跳串会更长,浪费,不合题意。
用一个vis数组记录是否被访问过,(vis[i][j])代表节点(i)是否以状态(j)被访问过,用来避免bfs时由于重复的字符串得不到最优解的情况
那么做一个bfs,每次队头取出一个结点和他在进队时处于的状态,枚举他的所有儿子,然后合并状态,再塞进队列里,等着下一次用
如果出现了状态填满了,就输出,这里利用一个小技巧,把之前处理的时候把状态编号指向他父亲的编号,输出方便

#include<bits/stdc++.h>
using namespace std;
#define ll int
#define ull unsigned long long 
#define ZZ_zuozhe int main()
#define NS 5005

ll n;
char tmp[55];
struct vertex
{
	ll fail;
	ll son[26];
	ll st;
}tree[NS];
ll vcn=0;
queue<ll> q;

void getfail()
{
	for(int i=0;i<26;i++)
	{
		if(tree[0].son[i])
		{
			q.push(tree[0].son[i]);
		}
	}
	while(!q.empty())
	{
		ll u=q.front();
		q.pop();
		//cout<<u<<endl;
		for(int i=0;i<26;i++)
		{
			if(tree[u].son[i])
			{
				tree[tree[u].son[i]].fail=tree[tree[u].fail].son[i];
				tree[tree[u].son[i]].st|=tree[tree[tree[u].fail].son[i]].st;
				q.push(tree[u].son[i]);
			}
			else tree[u].son[i]=tree[tree[u].fail].son[i];
		}
	}
}

bool vis[NS][1<<12|1];
char ans[NS];
ll nod=0;
ll fa[NS*(1<<12|1)],an[NS*(1<<12|1)];
ll tot=0;
queue<ll>q1,q2;

ZZ_zuozhe
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%s",tmp);
		ll len=strlen(tmp);
		ll rt=0;
		for(int j=0;j<len;j++)
		{
			ll t=tmp[j]-'A';
			if(!tree[rt].son[t])tree[rt].son[t]=++vcn;
			rt=tree[rt].son[t];
		}
		tree[rt].st|=(1<<(i-1));
	}
	getfail();
	vis[0][0]=1;
	q1.push(0);
	q2.push(0);
	ll tt=0;
	while(!q1.empty())
	{
		ll rt=q1.front(),st=q2.front();
		q1.pop();q2.pop();
		if(st==((1<<n)-1))
		{
			while(tt)
			{
				ans[++nod]=an[tt];
				tt=fa[tt];
			}
			for(int i=nod;i>0;i--)putchar(ans[i]+'A');
			return 0;
		}
		for(int i=0;i<26;i++)
		{
			if(!tree[rt].son[i])continue;
			if(!vis[tree[rt].son[i]][st|tree[tree[rt].son[i]].st])
			{
				vis[tree[rt].son[i]][st|tree[tree[rt].son[i]].st]=1;
				q1.push(tree[rt].son[i]);
				q2.push(st|tree[tree[rt].son[i]].st);
				fa[++tot]=tt;
				an[tot]=i;
			}
		}
		tt++;
	}
	return 0;
}

文本生成器

也是dp
既然让求可读文本总数,不妨反着想,用总方案数(26^n)减去不碰边界的方案数
其实跟前面那个病毒挺像的,都是标记危险节点,然后遍历的时候故意不走
(f[i][j])为在(j)点,有(i)位的时候,不碰边界的方案总数
那么

[f[i][tree[j].son[k]]=f[i-1][j] ]

最后快速幂求(26^n),再减去所有(f[m][~])的和,记得取模。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ZZ_zuozhe int main()
#define ull unsigned long long
#define P 10007
#define N 65
#define M 105
#define S 6005

ll n,m;

struct vertex
{
	ll fail;
	ll son[26];
	bool dg;
}tree[S];
char tmp[M];
ll vcn=0;

void ins()
{
	ll len=strlen(tmp);
	ll rt=0;
	for(int i=0;i<len;i++)
	{
		ll t=tmp[i]-'A';
		if(!tree[rt].son[t])tree[rt].son[t]=++vcn;
		rt=tree[rt].son[t];
	}
	tree[rt].dg=1;
}

void getfail()
{
	queue<ll>Q;
	for(int i=0;i<26;i++)if(tree[0].son[i])Q.push(tree[0].son[i]);
	while(!Q.empty())
	{
		ll rt=Q.front();
		Q.pop();
		for(int i=0;i<26;i++)
		{
			ll v=tree[rt].son[i];
			if(v)
			{
				tree[v].fail=tree[tree[rt].fail].son[i];
				tree[v].dg|=tree[tree[v].fail].dg;
				Q.push(v);
			}
			else tree[rt].son[i]=tree[tree[rt].fail].son[i];
		}
	}
}

ll f[M][S];

ll ksm(ll a,ll b)
{
	ll res=1;
	while(b)
	{
		if(b&1)res=res*a%P;
		a=a*a%P;
		b>>=1;
	}
	return res%P;
}

ll sol()
{
	f[0][0]=1;
	ll res=0;
	for(int i=1;i<=m;i++)
	{
		for(int j=0;j<=vcn;j++)
		{
			for(int k=0;k<26;k++)
			{
				if(!tree[tree[j].son[k]].dg)
				{
					f[i][tree[j].son[k]]+=f[i-1][j];
					f[i][tree[j].son[k]]%=P;
				}
			}
		}
	}
	res=ksm(26,m);
	ll tmp=0;
	for(int i=0;i<=vcn;i++)tmp=(tmp+f[m][i])%P;
	return ((res-tmp)%P+P)%P;
}

ZZ_zuozhe
{
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++)
	{
		scanf("%s",tmp);
		ins();
	}
	getfail();
	printf("%lld",sol());
	return 0;
}

背单词

警醒:线段树(rt<<1)打成(rt),白调(3h)
为什么今天全是(sb)错误啊qaq

AC自动机,dfs序,线段树
首先分析下题意:每个单词是后一个单词的子串,这个放在AC自动机里怎么理解呢
其实,每一个单词是后一个单词的子串,就是说后一个单词某一个结点的fail指向了前一个单词的结尾节点,此时后一个单词的前缀的后缀就是前一个单词,显然满足条件。
于是,问题转化为:前后两个单词满足后一个单词某一个结点的fail是前一个单词的结尾节点,从fail树的角度考虑,即在fail树中,后一个单词某结点在fail树以前一个单词结尾节点为根的子树中
关于子树的判断,dfs序是绝佳的选择
首先明确如果某个节点的价值,是他的fail树子树里任何一个节点都有机会享受到的,而dfs序是连续的,那么这就可以用线段树维护
因为取的是子序列,所以每次取出一个单词,枚举其中的每一个字母,选取当前能得到价值最大的一个节点(线段树维护最大值,单点查询),这个价值加上当前单词的价值,就是前(i)个单词的最大价值。
得到最大价值后,这个最大价值是可以让前一个单词结尾节点的fail子树内所有结点共享的,既然记录了dfs序,这里就可以用线段树的区间修改来解决。
这个过程中,如果遇到价值为负的单词,可以直接跳过,因为前面单词的选择不会对后面单词是否选择产生什么影响,那就干脆不要选负的。
在前面处理完毕后,枚举得到的每一个最大值,就可以得出全局的最大值。

#include<bits/stdc++.h>
using namespace std;
#define ll int
#define ull undigned long long
#define ZZ_zuozhe int main()
#define S 1000005
#define pb push_back
#define INF 100000000

ll T,n;
ll w[S];
char tmp[S];
vector<char> v[S];

struct trie
{
	ll fail;
	ll son[30];
	ll num;
}tree[S];
ll vcn=1;

void ins()
{
	ll len=strlen(tmp);
	ll rt=1;
	for(int i=0;i<len;i++)
	{
		ll t=tmp[i]-'a';
		if(!tree[rt].son[t])tree[rt].son[t]=++vcn;
		rt=tree[rt].son[t];
	}
	///
}

void getfail()
{
	queue<int> Q;
	tree[1].fail=1;
	for(int i=0;i<26;i++)
	{
		if(tree[1].son[i])tree[tree[1].son[i]].fail=1;
		Q.push(tree[1].son[i]);
	}
	while(!Q.empty())
	{
		ll u=Q.front();
		Q.pop();
		for(int i=0;i<26;i++)
		{
			ll v=tree[u].son[i];
			if(v)
			{
				tree[v].fail=tree[tree[u].fail].son[i];
				Q.push(v);
			}
			else tree[u].son[i]=tree[tree[u].fail].son[i];
		}
	}
} 

struct edge
{
	ll u,v;
}e[S<<1];
ll head[S],next[S],cnt=0;

void add(ll u,ll v)
{
	++cnt;
	e[cnt].u=u;
	e[cnt].v=v;
	next[cnt]=head[u];
	head[u]=cnt;
}

ll l[S],r[S],tt=0;
void dfs(ll rt)
{
	l[rt]=++tt;
	for(int i=head[rt];i;i=next[i])dfs(e[i].v);
	r[rt]=tt;
}

struct xds
{
	ll son[2];
	ll lz;
	ll val;
}t[S<<2];

#define lson rt<<1,l,m
#define rson rt<<1|1,m+1,r
inline void upd(ll rt){t[rt].val=max(t[rt<<1].val,t[rt<<1|1].val);}

void build(ll rt,ll l,ll r)
{
	t[rt].val=-INF;
	t[rt].lz=0;
	if(l==r)
	{
		return;
	}
	ll m=(l+r)>>1;
	build(lson);
	build(rson);
	upd(rt);
}

void pushdown(ll rt)
{
	if(t[rt].lz)
	{
		t[rt<<1].lz=max(t[rt<<1].lz,t[rt].lz);
		t[rt<<1|1].lz=max(t[rt<<1|1].lz,t[rt].lz);
		t[rt<<1].val=max(t[rt<<1].val,t[rt].lz);
		t[rt<<1|1].val=max(t[rt<<1|1].val,t[rt].lz);
		t[rt].lz=0;
	}
}

ll query(ll rt,ll l,ll r,ll nl,ll nr)
{
	if(nl<=l&&r<=nr)
	{
		return t[rt].val;
	}
	pushdown(rt);
	ll m=(l+r)>>1;
	ll res=0;
	if(m>=nl)res=max(res,query(lson,nl,nr));
	if(m<nr)res=max(res,query(rson,nl,nr));
	return res;
}

void update(ll rt,ll l,ll r,ll nl,ll nr,ll val)
{
	if(nl<=l&&r<=nr)
	{
		t[rt].val=max(t[rt].val,val);
		t[rt].lz=max(t[rt].lz,val);
		return;
	}
	pushdown(rt);
	ll m=(l+r)>>1;
	if(m>=nl)update(lson,nl,nr,val);
	if(m<nr)update(rson,nl,nr,val);
	upd(rt);
}

ll f[S];

void clear()
{
	memset(tree,0,sizeof tree);
	memset(e,0,sizeof e);
	memset(head,0,sizeof head);
	memset(next,0,sizeof next);
	cnt=1;
	memset(l,0,sizeof l);
	memset(r,0,sizeof r);
	tt=0;
	memset(t,0,sizeof t);
	for(int i=1;i<=n;i++)v[i].clear();
	memset(f,0,sizeof f);
}

ZZ_zuozhe
{
	//freopen("owo.in","r",stdin);
	//freopen("owo.out","w",stdout);
	scanf("%d",&T);
	while(T--)
	{
		//ll a=0;
		clear();
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
		{
			scanf("%s%d",tmp,&w[i]);
			//if(w[i]>0)a+=w[i];
			ins();
			ll len=strlen(tmp);
			for(int j=0;j<len;j++)v[i].pb(tmp[j]);
		}
		getfail();
		for(int i=2;i<=vcn;i++)add(tree[i].fail,i);
		dfs(1);
		build(1,1,tt);
		for(int i=1;i<=n;i++)
		{
			if(w[i]<=0)continue;
			ll len=v[i].size();
			ll rt=1,ma=0;
			for(int j=0;j<len;j++)
			{
				ll t=v[i][j]-'a';
				rt=tree[rt].son[t];
				ma=max(ma,query(1,1,tt,l[rt],l[rt]));
			}
			f[i]=max(ma,0)+w[i];
			update(1,1,tt,l[rt],r[rt],f[i]);
		}
		ll ans=-INF;
		for(int i=1;i<=n;i++)ans=max(ans,f[i]);
		printf("%d
",ans);
		//cout<<a<<' '<<tt<<' '<<vcn<<endl;
	}
	return 0;
}

好长啊我想学压行

密码

awsl
第一眼感觉是限长度版的最短母串,然后写假了,只有(20pts)
其实是挺像的,都用了状压……但是这个不太一样
最短母串里因为要求最短,所以我们转移的时候判了一个是否有儿子,但这个不要求最短,有时甚至必须有多余的字符,所以按照最短母串的思路是走不通的
之前在一篇题解里好像看见过“AC自动机+DP,一般都是设(f[i][j])(i)是当前匹配长度,(j)是当前节点,然后视情况再加一维其他信息”
这里我们用状压思想,所以就加上一维已有单词的状态,即设(f[i][j][S])(i,j)意义同上,(S)为当前状态
则有转移方程:

[f[i+1][tree[j].son[k]][S|tree[tree[j].son[k].st]=sum f[i][j][k] ]

做这样的题时,注意在(getfail)时同时将(fail)结点的状态合并至本结点。
答案即为(sum f[L][][S_满])

关于输出:
我们先用dfs预处理出所有结点的位置是否能通向一个符合要求的串,然后输出时利用他剪枝,如果不通就不走了。输出就是一边dfs一边存下当前答案,够长度了就输出
(这题不写输出给(50pts)???)
打算有时间来一遍二刷,不留代码了

[BJWC2011]禁忌

ac自动机版GT考试
首先要读懂题……这种带有剧情的题有时候会读着很麻烦……

  • 字母集(A)上的每个非空字符串对应了一个魔法。其中(A)是包含了前(alphabet)个小写字母的集合。
  • 有一个集合(T),包含了(N)个字母集(A)上的字符串。(T)中的每一串称为一个禁忌串(Taboo string)
  • 一个魔法,或等价地,其对应的串(s)因为包含禁忌而对使用者造成的伤害按以下方式确定:把(s)分割成若干段,考虑其中是禁忌串的段的数目,不同的分割可能会有不同的数目,其最大值就是这个伤害。

第一条这个(A)里头有一堆小写字母,每个非空字符串就是把这些字母随便拼(这条似乎无关紧要
第二条就是给一个集合(T),里面都是满足要求的(A)中的字符串,这些串叫禁忌串,一会会给
第三条就是在一个字符串中,有多少相互不重叠的禁忌串,就造成多大伤害

那么题中给出了所有禁忌串,现在需要我们求一个长度为(len)的,字母从(A)中取的,随机的串中禁忌串数量的期望(因为要达到题中的最大情况,就是在切的时候恰好把存在的每一个不重叠的禁忌串都切出来
考虑到多模式串,我们可以建出AC自动机
考虑匹配的过程,每个节点如果是一个禁忌串的末尾,就应当直接切,也就是前面扔掉,再从头进行匹配(假如此时有一个也正在匹配的更长的禁忌串,去匹配它显然不优)这个时候,对答案产生了贡献,我们把这部分贡献计入答案,并将它纳入之后的转移中。而如果这时并不能匹配成功,就枚举此节点所有的儿子并等概率转移。
转移类似于这个的意思:

[f_{i ightarrow j}=f_{i ightarrow k}*f_{k ightarrow j}; ]

这个形式就可以用矩阵加速。那么考虑初始矩阵。
我们的矩阵下标为节点编号,(M[i][j])代表的是匹配位置(i ightarrow j)的期望
另外,我们设(M^n)中的(M[i][vcn+1])为以(i)为开头,共有(n)位时对答案的总贡献,那么(M[0][vcn+1])即为答案。
设置初始矩阵:
(egin{cases} if(tree[tree[i].son[j]].dg)\ M_0[i][0]+=frac{1}{alphabet}(回到树根,供下一步转移)\ M_0[i][vcn+1]+=frac{1}{alphabet}(计入答案)\ else\ M_0[i][j]+=frac{1}{alphabet}(向下走一步)\ 特别地,设M_0[vcn+1][vcn+1]=1,便于转移 end{cases})
之后就是简单的矩阵快速幂啥的,代码很好写不放,期望题大概主要考察思路吧

原文地址:https://www.cnblogs.com/zzzuozhe-gjy/p/13370146.html