ybt2.5章AC自动机题解
算法理解
即在字典树上跑kmp
T1:
根据这个结论我自己手搓了一个AC自动机上去,喜提TLE
我是如何操作的呢?
我当时的想法是这样的:我们把字典树从根到该节点形成的链看成是一个模式串与文本串进行匹配,然后就用一个dfs来传递j就可以解决了
然后我打开书一看到这幅图,立马就不淡定了
我dfs可能nxt数组并未 \(nxt[u]\) 在u之前更新到
所以我们只好采取bfs
然后洛谷上过了,ybt又TLE了
原因是我没有用一个小小的优化:
if(!tr[x][i]){
tr[x][i]=tr[nxt[x]][i];
continue;
}
如果这个地方本身就没有字符,那么意味着我们就要往前跳了,我们考虑正常的kmp跳法就是跳到有字符了为止,也就是说我们要跳的位置都是没有字符的,那我们就把这个位置修改为第一个有值的位置,下一次跳直接一步到位。
最后统计答案时直接就能访问到第一个合法状态,然后在暴力往上跳所有nxt然后统计有哪些被访问到了,因为跳一次nxt可能只是深度-1,所以因为题目中有限制字典树深度最多为50,所以复杂度为 \(O(M*50)\)
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+50;
int t,n,tot,m,num;
int tr[N][30],en[N],nxt[N],ans[N],gg[N],q[N];
char s[N];
void add(int x){
int len=strlen(s+1),r=0;
for(int i=1;i<=len;i++){
int id=(s[i]-'a');
if(!tr[r][id]) tr[r][id]=++tot;
r=tr[r][id];
}
gg[r]++;
en[r]=x;
// printf("r=%d %d %d\n",r,gg[r],en[r]);
}
void bfs(){
q[1]=0;
for(int l=1,r=1;l<=r;l++){
for(int i=0;i<26;i++){
int x=q[l],y=nxt[q[l]];
if(!tr[x][i]){
tr[x][i]=tr[nxt[x]][i];
continue;
}
while(y&&!tr[y][i]){
y=nxt[y];
}
if(tr[y][i]&&x){
y=tr[y][i];
}
nxt[tr[x][i]]=y;
q[++r]=tr[x][i];
}
}
}
int main(){
// freopen("P3808_2.in","r",stdin);
scanf("%d",&t);
// t=1;
while(t--){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%s",s+1);
add(i);
}
bfs();
// kmp(0,0);
// for(int i=1;i<=tot;i++){
// printf("%d %d\n",nxt[i]);
// }
scanf("%s",s+1);
m=strlen(s+1);
for(int i=1,j=0;i<=m;i++){
int id=(s[i]-'a');
int k=tr[j][id];
j=tr[j][id];
while(k){
num+=gg[k];
gg[k]=0;
k=nxt[k];
}
}
printf("%d\n",num);
for(int i=0;i<=tot;i++){
en[i]=nxt[i]=ans[i]=gg[i]=0;
for(int j=0;j<26;j++){
tr[i][j]=0;
}
}
tot=num=0;
}
}
T2:
加强版模板
没有告诉你字典树的深度,所以最后暴力跳一次最劣的复杂度为 \(O(1e6)\)
不能接受
我们考虑一种类似于懒标记的做法
跳到哪个位置我们将其打上标记,因为最终我们要把根据nxt索引所得到的一串链都打上标记,所以在最后我们把标记根据nxt上传即可
所以一个位置的答案是所有nxt数组指向它的答案的加和,我们要在访问到这个元素之前把标记全部上传完,于是就联想到拓扑排序
然后注意一些细节,只有那些有字符存在的点才能统计入度,所以要在bfs时统计各个点的入度
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=205,M=1e6+5;
int n,tot;
int tr[M][26],ru[M],q[M],nxt[M],vis[M],ans[M];
char s[N][M/10];
vector<int>en[M];
void add(int x){
int len=strlen(s[x]+1),r=0;
for(int i=1;i<=len;i++){
int g=(s[x][i]-'a');
if(!tr[r][g]) tr[r][g]=++tot;
r=tr[r][g];
}
en[r].push_back(x);
}
void bfs(){
q[1]=0;
for(int l=1,r=1;l<=r;l++){
int x=q[l];
for(int i=0;i<26;i++){
if(!tr[x][i]) tr[x][i]=tr[nxt[x]][i];
else{
if(x>0) nxt[tr[x][i]]=tr[nxt[x]][i];
ru[nxt[tr[x][i]]]++;
q[++r]=tr[x][i];
}
}
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%s",s[i]+1);
add(i);
}
bfs();
for(int i=1;i<=n;i++){
int len=strlen(s[i]+1),r=0;
for(int j=1;j<=len;j++){
int g=s[i][j]-'a';
r=tr[r][g];
vis[r]++;
}
}
int q1=1,q2=0;
for(int i=1;i<=tot;i++){
if(!ru[i]) q[++q2]=i;
// printf("%d %d\n",nxt[i],ru[i]);
}
while(q1<=q2){
int x=q[q1];
q1++;
for(int i:en[x]){
ans[i]+=vis[x];
}
vis[nxt[x]]+=vis[x];
ru[nxt[x]]--;
// printf("%d %d %d\n",x,nxt[x],ru[nxt[x]]);
if(!ru[nxt[x]]) q[++q2]=nxt[x];
}
for(int i=1;i<=n;i++){
printf("%d\n",ans[i]);
}
return 0;
}
T3:
基本上和T2一样,把每一个点标记上是哪个字符串和字符串的哪个位置经过这个点存储下来,判断这个点是否被访问过即可
注意要是用vector存的话会MLE,我们反过来判断一个字符对应的位置是否被访问过即可
T4:
你会发现删掉一段后前面统计的r依旧适用,所以我们维护一个栈,每个点统计完以后压入栈,然后找到一段合法的串就将栈中这个长度的值弹出,然后用栈顶的r继续往下转移即可
一个未定义错误(警钟敲烂):
\(a[++l]=a[l]\)
这会在不同编译器有不同的结果
T5:
呵呵呵,看了好久然后把自己绕晕了,让我们理清一下思路
首先,要构成一个无限长度的串,然后这个串要在AC自动机上匹配并且不能匹配到一个串的末尾
所以我们就模拟这个匹配的过程,考虑何时这个无限串是合法的?
就是这个串在匹配的时候不会匹配到一个串的末尾,并且它还能匹配到原来已经匹配过的位置形成一个环,这就说明沿着这条路径匹配一定不会匹配到一个串的末尾处,所以存在一种串使得成立
我们怎么实现呢?
可以用一个dfs来暴力实现,再加一个小剪枝,如果这个点之前搜过并且不合法,就跳过它
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=3e4+5;
int n,tot,cnt;
int tr[N][2],num[N],nxt[N],q[N],vis[N],f[N];
char s[N];
void add(){
int len=strlen(s+1),r=0;
for(int i=1;i<=len;i++){
int g=s[i]-'0';
if(!tr[r][g]) tr[r][g]=++tot;
r=tr[r][g];
}
num[r]=1;
}
void bfs(){
q[1]=0;
for(int l=1,r=1;l<=r;l++){
int x=q[l];
for(int i=0;i<=1;i++){
if(!tr[x][i]) tr[x][i]=tr[nxt[x]][i];
else{
if(x>0) nxt[tr[x][i]]=tr[nxt[x]][i];
if(num[nxt[tr[x][i]]]) num[tr[x][i]]=1;
q[++r]=tr[x][i];
}
}
}
}
void dfs(int x){
if(cnt) return;
vis[x]=1;
for(int i=0;i<=1;i++){
if(vis[tr[x][i]]){
// for(int i=0;i<=tot;i++){
// if(vis[i]) printf("%d ",i);
// }
// printf(" %d\n",tr[x][i]);
cnt=1;
return;
}
else if(!num[tr[x][i]]&&!f[tr[x][i]]){
f[tr[x][i]]=1;
dfs(tr[x][i]);
}
}
vis[x]=0;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%s",s+1);
add();
}
bfs();
dfs(0);
if(cnt) printf("TAK\n");
else printf("NIE\n");
return 0;
}
T6:
转化一下,我们将结尾点和nxt指向结尾点的点点权+1,预处理后,就是要求从0出发,每个点有个点权,然后至多走k次,求出来能获得的最大点权
一眼dp,设 \(dp[i][j]\) 为走到i点,走j次能获得的最大点权,至于连边就是tr所连的三条边转移即可
T7:
模板
T8:
字典树+dp
直接切了(算是第一道切掉的紫吧)
数数题,dp,设 \(dp[i][j][0/1]\) 代表生成长度为i的最后停留在j的是否包含一个完整的关键词的方案数
然后就从0开始递推,枚举所有tot点来转移即可
T9:
不懂这道题和AC自动机有什么关系。。。
有一个显然的贪心结论,就是一个串从头到尾完整匹配是最优的,如果一个串匹配一半然后就匹配其他串了,是不优的
所以我们只需要考虑爆搜这几个串出现的顺序即可
考虑两种情况
1.两个字符串存在包含关系,显然那个被包含的字符串就不需要要了,我们用 \(find\) 函数把它们找出来并删掉即可
2.两个字符串前缀和后缀重叠一部分,我们预处理出最长重叠长度,然后爆搜再统计答案即可
T10:
也没有想象中的那么难,可能是被吓到了,没有多想,但是感觉再想一想是能想出来的
首先滤清一下题意,首先这个人是记住所有单词的,其次,就是这个人在开始读之前,\(c[i]\) 都为0,所以这个人遗忘单词的顺序是固定的,我们可以跑一遍AC自动机统计 \(c[i]\) ,然后排序,因为 \(c[i]\) 相同的串是同时进行决策的,所以再去重
然后题目就简化成了一道很显然的概率dp
最开始概率都为1
然后我们进行k次操作
设 \(dp[i][j]\) 为进行i次操作后,j串没有被遗忘的概率
所以转移式子就是
解释一下,两种情况:
- j-1上一轮被记住,这一轮j一定不会被遗忘
- j-1上一轮被忘记,这一轮j有 (j-1上一轮被忘记的概率)*p 的概率不被遗忘
然后这道题就做完了