哈哈哈哈我学明白了也许吧。挂几篇题解:
b站的这个up讲得太好啦:https://www.bilibili.com/video/BV1uJ411Y7Eg?p=3&vd_source=f51708a79de2172437e75278f8c832e0
(https://blog.youkuaiyun.com/bestsort/article/details/82947639)与(https://www.cnblogs.com/cjyyb/p/7196308.html)呃还有(https://oi-wiki.org/string/ac-automaton/)这三个。
就是一种对字典树的优化吧,差不多咯,步骤呢也不多,不过前置知识是kmp与trie树(其实kmp还行,不是很行),第一步,先建一颗trie,然后捏,就做失配指针,失配指针是什么捏,就是呀,那个那个如果自己无法继续向下匹配了,那就只能换一部分去向下去问啦,比如说
对于一张这样的(抄的)图,如果你有一个sher的匹配串串,那你肯定要先左右左走到she那,然后你发现,后面没r了,那怎么办,那就换一边呗,用大佬的话说:Trie树的失配指针是指向是沿着其父节点 的 失配指针,一直向上,直到找到拥有当前这个字母的子节点 的节点 的那个子节点。所以我们可以换到最右链,her的,不过呢两个问题,1是第二层的节点的指针要手动打,第二就是,不存在的点也需要失配指针来维护。
或者我们可以将失配指针指向的的节点理解为:
当前节点所代表的串,最长的、能与后缀匹配的,在Trie中出现过的前缀所代表的节点。所以,fail指针类似于kmp的next数组,只不过由单串变为了多串而已
我自己的理解,便是,你要让最后那位是当前的这个字符嘛,后面的改不了,那就只能删点前面的咯,然后就一步步的删,向上缩嘛,但是要它尽量长啊对吧,所以就一层一层的向上咯重要的是
代码(大概是不知道从哪里学来的):
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<queue>
#include<algorithm>
using namespace std;
struct Tree//字典树
{
int fail;//失配指针
int vis[26];//子节点的位置
int end;//标记有几个单词以这个节点结尾
}AC[1000000];//Trie树
int cnt=0;//Trie的指针
inline void Build(string s)
{
int l=s.length();
int now=0;//字典树的当前指针
for(int i=0;i<l;++i)//构造Trie树
{
if(AC[now].vis[s[i]-'a']==0)//Trie树没有这个子节点
AC[now].vis[s[i]-'a']=++cnt;//构造出来
now=AC[now].vis[s[i]-'a'];//向下构造
}
AC[now].end+=1;//标记单词结尾
}
void Get_fail()//构造fail指针
{
queue<int> Q;//队列
for(int i=0;i<26;++i)//第二层的fail指针提前处理一下
{
if(AC[0].vis[i]!=0)
{
AC[AC[0].vis[i]].fail=0;//指向根节点
Q.push(AC[0].vis[i]);//压入队列
}
}
while(!Q.empty())//BFS求fail指针
{
int u=Q.front();
Q.pop();
for(int i=0;i<26;++i)//枚举所有子节点
{
if(AC[u].vis[i]!=0)//存在这个子节点
{
AC[AC[u].vis[i]].fail=AC[AC[u].fail].vis[i];
//子节点的fail指针指向当前节点的
//fail指针所指向的节点的相同子节点
Q.push(AC[u].vis[i]);//压入队列
}
else//不存在这个子节点
AC[u].vis[i]=AC[AC[u].fail].vis[i];
//当前节点的这个子节点指向当
//前节点fail指针的这个子节点
}
}
}
int AC_Query(string s)//AC自动机匹配
{
int l=s.length();
int now=0,ans=0;
for(int i=0;i<l;++i)
{
now=AC[now].vis[s[i]-'a'];//向下一层
for(int t=now;t&&AC[t].end!=-1;t=AC[t].fail)//循环求解
{
ans+=AC[t].end;
AC[t].end=-1;//防止解被重复使用
}
}
return ans;
}
int main()
{
int n;
string s;
cin>>n;
for(int i=1;i<=n;++i)
{
cin>>s;
Build(s);
}
AC[0].fail=0;//结束标志
Get_fail();//求出失配指针
cin>>s;//文本串
cout<<AC_Query(s)<<endl;
return 0;
}
自己的:
#include<bits/stdc++.h>
using namespace std;
int n,m;
struct tr
{
int zz;
int son[30];
int sum;
};
tr ac[5000000];
int tot=0;
void bt(char p[])
{
int len=strlen(p+1);
int now=0;
for(int i=1;i<=len;i++)
{
int x=p[i]-'a'+1;
if(ac[now].son[x]==0) ac[now].son[x]=++tot;
now=ac[now].son[x];
}
ac[now].sum++;
return ;
}
void zz()
{
queue<int> q;
for(int i=1;i<=26;i++)
{
if(ac[0].son[i]!=0)
{
ac[ac[0].son[i]].zz=0;
q.push(ac[0].son[i]);
}
}
while(q.size()!=0)
{
int x=q.front();
q.pop();
for(int i=1;i<=26;i++)
{
if(ac[x].son[i]!=0)
{
ac[ac[x].son[i]].zz=ac[ac[x].zz].son[i];
q.push(ac[x].son[i]);
}
else ac[x].son[i]=ac[ac[x].zz].son[i];
}
}
}
int find(char p[])
{
int len=strlen(p+1);
int now=0,ans=0;
for(int j=1;j<=len;j++)
{
int x=p[j]-'a'+1;
now=ac[now].son[x];
for(int i=now;i>0&&ac[i].sum!=-1;i=ac[i].zz)
{
ans+=ac[i].sum;
ac[i].sum=-1;
}
}
return ans;
}
char s[1000001];
int main()
{
tot=0;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%s",s+1);
bt(s);
}
ac[0].zz=0;
zz();
scanf("%s",s+1);
printf("%d\n",find(s));
return 0;
}
P3796 【模板】AC 自动机(加强版)这道和上面板子差不多啦,
不过还是有些细节什么的但不管咯,代码如下:
#include<bits/stdc++.h>
using namespace std;
int n,m;
int tot=0;
char s[160][1001];
int ll[10001];
struct TREE
{
int fail,vis[27],sum;
};
TREE ac[100001];
char ss[1000021];
void bt(char a[],int len,int num)
{
int now=0;
for(int i=1;i<=len;i++)
{
if(ac[now].vis[a[i]-'a'+1]==0) ac[now].vis[a[i]-'a'+1]=++tot;
now=ac[now].vis[a[i]-'a'+1];
}
ac[now].sum=num;
return ;
}
int q[1000001];
void getfail()
{
int st=1,ed=1;
for(int i=1;i<=26;i++)
{
if(ac[0].vis[i]!=0)
{
ac[ac[0].vis[i]].fail=0;
q[ed]=ac[0].vis[i];
ed++;
}
}
while(st!=ed)
{
int x=q[st];
for(int i=1;i<=26;i++)
{
if(ac[x].vis[i]!=0)
{
ac[ac[x].vis[i]].fail=ac[ac[x].fail].vis[i];
q[ed]=ac[x].vis[i];ed++;
// if(ed>100000) ed=1;
}
else ac[x].vis[i]=ac[ac[x].fail].vis[i];
}
st++;
// if(st>100000) st=1;
}
return ;
}
struct node
{
int num,pos;
} ans[1000001];
void find(char p[],int len)
{
int now=0;
for(int i=1;i<=len;i++)
{
now=ac[now].vis[p[i]-'a'+1];
for(int t=now;t;t=ac[t].fail) ans[ac[t].sum].num++;
}
return ;
}
bool cmp(const node &x,const node &y)
{
if(x.num!=y.num) return x.num>y.num;
return x.pos<y.pos;
}
int main()
{
while(scanf("%d",&n),n!=0)
{
tot=0;
memset(ans,0,sizeof(ans));
memset(ac,0,sizeof(ac));
for(int i=1;i<=n;i++)
{
scanf("%s",s[i]+1);
ans[i].pos=i,ans[i].num=0;
ll[i]=strlen(s[i]+1);
bt(s[i],ll[i],i);
}
ac[0].fail=0;
getfail();
scanf("%s",ss+1);
int len=strlen(ss+1);
find(ss,len);
sort(ans+1,ans+n+1,cmp);
printf("%d\n",ans[1].num);
for(int i=1;i<=ll[ans[1].pos];i++) printf("%c",s[ans[1].pos][i]);
int k=2;
while(ans[1].num==ans[k].num)
{
printf("\n");
for(int i=1;i<=ll[ans[k].pos];i++) printf("%c",s[ans[k].pos][i]);
k++;
}
printf("\n");
}
return 0;
}
烦死了,调了一下午发现主函数里忘getfail()了nmd!然后这个卡范围还是挺麻烦的。刚刚又发现一篇不错的bk(https://ouuan.github.io/post/ac%E8%87%AA%E5%8A%A8%E6%9C%BA%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/)。
嗯,第一题呀:P3966 [TJOI2013]单词呃,打完之后我对其fail的理解加深一百倍!就是说首先,我们都知道fail树就是在跑ac机中把失配指针所指向的值与失配指针所表示的边重新建出来的树。有这个思想,那么这道题就很简单了。
跑一遍AC自动机,每一个节点保存一下属于多少字符串,为它的权值。然后一个节点表示的字符串在整个字典中出现的次数相当于其在Fail树中的子树的权值的和。
#include<bits/stdc++.h>
using namespace std;
int n,m;
int tot=0;
char s[2000001];
int wz[2000001];
struct trr
{
int son[30],sum,fail;
};
trr ac[2000001];
int sz[2000001];
void bt(char a[],int len,int w)
{
int now=0;
for(int i=1;i<=len;i++)
{
int x=a[i]-'a'+1;
if(ac[now].son[x]==0) ac[now].son[x]=++tot;
now=ac[now].son[x];
sz[now]++;
}
wz[w]=now;
return ;
}
int q[2000001];
void getfail()
{
int st=1,ed=1;
ac[0].fail=0;
for(int i=1;i<=26;i++)
{
if(ac[0].son[i]!=0) q[ed++]=ac[0].son[i];
}
while(st!=ed)
{
int now=q[st];
for(int i=1;i<=26;i++)
{
if(ac[now].son[i]!=0)
{
ac[ac[now].son[i]].fail=ac[ac[now].fail].son[i];
q[ed++]=ac[now].son[i];//同时也用来排序,来控制串串的那个位置,怎么说捏,就是从后往前了?感性理解。
}
else ac[now].son[i]=ac[ac[now].fail].son[i];
}
st++;
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%s",s+1);
int len=strlen(s+1);
bt(s,len,i);
}
getfail();//前面都可以正常维护
for(int i=tot;i>=1;i--) sz[ac[q[i]].fail]+=sz[q[i]];//就是这神奇的一句啦,意思是后面有的前面也有,所以便可以继承过去嘿嘿,理解理解
for(int i=1;i<=n;i++) printf("%d\n",sz[wz[i]]);
return 0;
}
下一个就是类似于那个kmp加栈的那玩意,P3121 [USACO15FEB]Censoring G
直接看我维护吧主要是多了一个深度要处理
#include<bits/stdc++.h>
using namespace std;
int n,m;
int tot=0;
int q[1000001];
char ans[1000001];
int dep[1000001];
struct trr
{
int son[30],fail,sum;
};
trr ac[1000001];
void bt(char a[],int len)
{
int now=0;
for(int i=1;i<=len;i++)
{
int x=a[i]-'a'+1;
if(ac[now].son[x]==0) ac[now].son[x]=++tot;
now=ac[now].son[x];
}
ac[now].sum=1;
return ;
}
void getfail()
{
int st=1,ed=1;ac[0].fail=0;
for(int i=1;i<=26;i++)
{
if(ac[0].son[i]!=0)
{
dep[ac[0].son[i]]=1;//初始化第二层的深度,其实就是字符串长度啦
ac[ac[0].son[i]].fail=0;
q[ed++]=ac[0].son[i];
}
}
while(st!=ed)
{
int now=q[st];
for(int i=1;i<=26;i++)
{
if(ac[now].son[i]!=0)
{
ac[ac[now].son[i]].fail=ac[ac[now].fail].son[i];
q[ed++]=ac[now].son[i];
dep[ac[now].son[i]]=dep[now]+1; //继承它一手~
}
else ac[now].son[i]=ac[ac[now].fail].son[i];
}
st++;
}
return ;
}
int r=0;
void find(char s[],int len)//直接拿栈
{
int now=0;q[0]=0;
for(int i=1;i<=len;i++)
{
int x=s[i]-'a'+1;now=ac[now].son[x];
q[++r]=now;
ans[r]=s[i];
if(ac[now].sum) r-=dep[now],now=q[r];//减去字符串的长度部分啦
}
}
int main()
{
char s1[1000001],s2[1000001];
scanf("%s",s1+1);
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%s",s2+1);
int len=strlen(s2+1);
bt(s2,len);
}
getfail();
int len=strlen(s1+1);
find(s1,len);
for(int i=1;i<=r;i++) printf("%c",ans[i]);
return 0;
}
下一个是——P3041 [USACO12JAN]Video Game G题目先生!不过好像每次看ac自动机题解的时候都能看到yyb大佬的题解,太巨!呃其实这题就是一道ac自动机+dp嘛,转移很显然从Trie树的节点跳到他的儿子节点于是f [ i ] [ j ]表示长度为i,当前在 ac树 上的j位置时的最大得分,在求fail的时候可以完成值的累加咯, 最后一步就是转移啦。但是要注意一个问题,在计算的时候,每一个节点加入后能够造成的贡献,要加上他的子串的贡献。好代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,k;
struct trr
{
int son[4],fail,sum;
};
trr ac[100001];
int tot=0,ans=0;
void bt(char s[],int len)
{
int now=0;
for(int i=1;i<=len;i++)
{
int x=s[i]-'A'+1;
if(ac[now].son[x]==0) ac[now].son[x]=++tot;
now=ac[now].son[x];
}
ac[now].sum++;
return ;
}
int q[100001];
void getfail()
{
int st=1,ed=1;ac[0].fail=0;
for(int i=1;i<=3;i++)
{
if(ac[0].son[i]!=0)
{
ac[ac[0].son[i]].fail=0;
q[ed++]=ac[0].son[i];
}
}
while(st!=ed)
{
int now=q[st];
for(int i=1;i<=3;i++)
{
if(ac[now].son[i]!=0)
{
ac[ac[now].son[i]].fail=ac[ac[now].fail].son[i];
q[ed++]=ac[now].son[i];
}
else ac[now].son[i]=ac[ac[now].fail].son[i];
}
ac[now].sum+=ac[ac[now].fail].sum;//极其高明的一句转移,将前面能和自己匹配的那个串串的值转移到自己身上
st++;
}
return ;
}
int f[2000][1001];
void dp()//精华呀我的妈,就是继承转移与dp~
{
for(int len=0;len<=k;len++)//长度也可能为0呀qwq
{
for(int i=1;i<=tot;i++) f[len][i]=-1e9;//当然是最小啦
}
for(int len=1;len<=k;len++)
{
for(int i=0;i<=tot;i++)
{
for(int j=1;j<=3;j++)
{
f[len][ac[i].son[j]]=max(f[len][ac[i].son[j]],f[len-1][i]+ac[ac[i].son[j]].sum);
//我一个儿子的最大值当然是我的值加上它(累加完)的值啦,注意len的定义哦
}
}
}
for(int i=0;i<=tot;i++) ans=max(ans,f[k][i]);
return ;
}
char s[100001];
int main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
{
scanf("%s",s+1);
int len=strlen(s+1);
bt(s,len);
}
getfail();
dp();
printf("%d",ans);
return 0;
}
下一题,有点小怀念,诶呀不说题外话!P4052 [JSOI2007]文本生成器,就说这题比较奇怪,呃采用正难则反的思路,算出总数是26^m,然后不合法的是不知道什么鬼,然后就可以知道合法了的啦,然后维护的方式就是建一颗ac树嘛,然后累加所有不在树上的节点就行啦。大概就是:
定义f[i][j]表示当前在j点且串长为i时不经过单词结尾的路径条数,然后从父亲往儿子转移即可
注意如果一个单词的后缀是一个可读单词(即fail指针指向可读单词的结尾节点),那么这个单词一定也是可读的,我们就不能往这个单词走了这个dp的理解确实深刻。
维护的话要特别讲一下的,就是使我对dp的理解更进一步,是什么捏,抄一下啊别人的
for(int i=1;i<=m;i++)//每一个点
for(int j=1;j<=t.cnt;j++)//全点参与
for(int c=1;c<=26;c++) //沿着子节点生成文本串下一个字母
if(!t.mk[t.ch[j][c]]) //避开模式串
dp[i][t.ch[j][c]]=(dp[i][t.ch[j][c]]+dp[i-1][j])%mod;//自己可以给儿子传多少,并且是上一层的自己才能给儿子传。
//不过吐槽一句不知道能不能用滚动数组优化?。
有的人不能理解为什么这样能传递值啊,你先看看这一段
if(ac[now].son[i]!=0)
{
ac[ac[now].son[i]].fail=ac[ac[now].fail].son[i];
ac[ac[now].son[i]].sum|=ac[ac[ac[now].son[i]].fail].sum;//现在不说这句QWQ
q[ed++]=ac[now].son[i];
}
else ac[now].son[i]=ac[ac[now].fail].son[i];//如果我的儿子不存在,我将把给它的值,传到我fail的那个儿子上
所以本质上来说,它只是把传到虚空中的值给了那个存在的节点(
我的代码就来啦:
using namespace std;
int n,m;
struct trr
{
int son[30],fail,sum;
};
trr ac[1000001];
int tot=0;
int f[200][100001];
void bt(char a[],int len)
{
int now=0;
for(int i=1;i<=len;i++)
{
int x=a[i]-'A'+1;
if(ac[now].son[x]==0) ac[now].son[x]=++tot;
now=ac[now].son[x];
}
ac[now].sum=1;
return ;
}
int q[1000001];
void getfail()
{
int st=1,ed=1;ac[0].fail=0;
for(int i=1;i<=26;i++)
{
if(ac[0].son[i]!=0)
{
ac[ac[0].son[i]].fail=0;
q[ed++]=ac[0].son[i];
}
}
while(st!=ed)
{
int now=q[st];
for(int i=1;i<=26;i++)
{
if(ac[now].son[i]!=0)
{
ac[ac[now].son[i]].fail=ac[ac[now].fail].son[i];
ac[ac[now].son[i]].sum|=ac[ac[ac[now].son[i]].fail].sum;//判断与自己后缀相同的那个串有解否然后就看看能不能继承咯
q[ed++]=ac[now].son[i];
}
else ac[now].son[i]=ac[ac[now].fail].son[i];
}
st++;
}
}
int ksm(int x,int c)
{
int rt=1;
while(c>0)
{
if(c%2==1) rt=(rt*x)%10007;
x*=x;x%=10007;
c=c>>1;
}
return rt;
}
char s[100001];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%s",s+1);
int len=strlen(s+1);
bt(s,len);
}
getfail();
f[0][0]=1;//初始化极其重要
for(int i=1;i<=m;i++)//因为初始化的缘故所以不会多算
{
for(int j=0;j<=tot;j++)//包括根节点也要算啊因为其实算的是它的儿子
{
for(int k=1;k<=26;k++)
{
if(!ac[ac[j].son[k]].sum) f[i][ac[j].son[k]]=(f[i][ac[j].son[k]]+f[i-1][j])%10007;//向下传递咯(又或者向上?
}
}
}
int sum=ksm(26,m);
for(int i=0;i<=tot;i++) sum=(sum-f[m][i]+10007)%10007;
printf("%d",sum);//%10007
return 0;
}
下一道明天再补吧,估计难了。呜呜呜我还是含泪开始写了:CF163E e-Government感觉麻烦的要死,是ac自动机套树状数组(恶我已经抛弃线段树了嘛)不过还是恶心的一匹,难受死了那个空间与时间卡爆我,差一点没过呜呜呜,还好我开了o2不然就g了。
用别人的题解哈:不难发现,一个节点的 endend 为在它自身结尾的字符串数加上它在 fail 树上的祖先节点结尾的字符串树。所以,删去一个字符串就是找到它尾部字符所对应的节点,然后将 fail 树中此节点的子树上的每个点(也就是每个以此节点为祖先的节点)的 endend 值 −1。换句话说,删除和添加操作就是在一颗树上每次找一个节点,将它子树中所有点的权值加 1 或减 1。同理,询问操作就是在树上询问一个节点的权。在预处理出这棵树的 dfs 序后,操作就可以了区间修改、单点查询的裸线段树/树状数组维护。
说说我犯病的问题,可恶,我居然没看出它是差分,树状数组用来维护的是差分,所以xr是cnt+1。好好想想为什么可恶啊草。上代码:
这样的时间相对直接查询会更优一些啦。
#include<bits/stdc++.h>
#define lowbit(x) x&(-x)
//#define int long long
using namespace std;
int n,m;
int cnt,tot;
bool v[1000009];
int f[1000009];
int id[1000009];//保存那些有值的点
struct trr
{
int son[30],fail,xl,xr;
};
trr ac[1000009];
int last[1000009];
struct pp
{
int x,y,next;
};
pp p[1000009];
void add(int x,int y,int k)
{
for(x;x<=cnt;x+=lowbit(x)) f[x]+=k;//明显的差分啊愚蠢bh
for(y;y<=cnt;y+=lowbit(y)) f[y]-=k;
return ;
}
int find(int x)
{
int rt=0;
for(x;x>=1;x-=lowbit(x)) rt+=f[x];
return rt;
}
void ins(int x,int y)
{
int now=++cnt;
p[now]={x,y,last[x]};
last[x]=now;
return ;
}
void bt(char s[],int len,int num)
{
int now=0;
for(int i=1;i<=len;i++)
{
int x=s[i]-'a'+1;
if(ac[now].son[x]==0) ac[now].son[x]=++tot;
now=ac[now].son[x];
}
id[num]=now;
return ;
}
int q[1000009];
void getfail()
{
int st=1,ed=1;ac[0].fail=0;
for(int i=1;i<=26;i++)
{
if(ac[0].son[i]!=0)
{
ac[ac[0].son[i]].fail=0;
q[ed++]=ac[0].son[i];
ins(0,ac[0].son[i]);
}
}
while(st!=ed)
{
int now=q[st];
for(int i=1;i<=26;i++)
{
if(ac[now].son[i]!=0)
{
ac[ac[now].son[i]].fail=ac[ac[now].fail].son[i];
ins(ac[ac[now].son[i]].fail,ac[now].son[i]);//顺便建个边啦
q[ed++]=ac[now].son[i];
}
else ac[now].son[i]=ac[ac[now].fail].son[i];
}
st++;
}
return ;
}
void dfs(int x)//处理出每个点的dfs序号以及其子树的其实用于搞那个树状数组
{//不过感觉这个树状数组的建法很奇怪欸,qw说是差分
ac[x].xl=++cnt;
for(int i=last[x];i!=-1;i=p[i].next) dfs(p[i].y);
ac[x].xr=cnt+1;
}
char a[1000009];
int answer(char s[],int len)
{
int now=0,rt=0;
for(int i=2;i<=len;i++)
{
now=ac[now].son[s[i]-'a'+1];
rt+=find(ac[now].xl);
}
return rt;
}
int main()
{
memset(last,-1,sizeof(last));
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++)
{
scanf("%s",a+1);
int len=strlen(a+1);
bt(a,len,i);
}
getfail();
cnt=0;dfs(0);
for(int i=1;i<=n;i++) add(ac[id[i]].xl,ac[id[i]].xr,1);
memset(v,true,sizeof(v));
for(int i=1;i<=m;i++)
{
scanf("%s",a+1);
int len=strlen(a+1);
if(a[1]=='?') printf("%d\n",answer(a,len));
else
{
int k=0;
for(int i=2;i<=len;i++)
{
k*=10;
k+=a[i]-'0';
}
if(a[1]=='-')
{
if(v[k]==true)
{
v[k]=false;
add(ac[id[k]].xl,ac[id[k]].xr,-1);
}
}
else
{
if(v[k]==false)
{
v[k]=true;
add(ac[id[k]].xl,ac[id[k]].xr,1);
}
}
}
}
return 0;
}
下一个题目是那个奇怪之题P2444 [POI2000]病毒话说要不是标签有我是真想不到的,不过题解说的好啊!呃呃呃ac自动机维护的是匹配的最多字符串嘛,那我们现在要没有,那我们就必须在字符串危险前用fail转移走嘛,那其实就可以建一个图,看看上面有没有环,如果有那就可以形成无限字符串啦,不过捏,注意要建边,可建边的时候不能建危险点的哈。
还是放一下别人的题解吧:
来一波逆向思维。假设我们构造出了一个无限长的安全代码,再拿到AC自动机上匹配,会发生什么?
没错,当我们一位一位地匹配的时候,我们会发现,永远都不会跳到某个病毒代码段结尾的位置(以后把这里称作危险节点,因为匹配到此处表明已经出现了某个病毒代码段),然后似乎会在自动机里永无止境地打转转。。。。。。
既然这个自动机又像一个图,那我们的问题不就变成了——在AC自动机(trie图)中寻找一个环,并且环上没有任何危险节点,并且还要注意,这个环能被根节点访问到(也就是说从根节点出发能在不经过危险节点的情况下走到到这个环,不然在模拟AC自动机匹配的时候无法到达这个这个环,也就失去了意义,楼上Dalao这里可能表述不尽准确)。
好像还有些注意事项,因为是有向边,所以就是判断环的时候要用两个数组,一个维护是否走过,另一个维护是否是路径上的点,这个问题想了一会,就只是用一个数组的话会有误判的情况的呀比如说一个 1 -> 2 ,1 -> 3 ,2 -> 4,3 -> 4的情况如果走完1到2到4后,再走1到3的话只用一个数组就会判定有环,所以要用另一个数组来记录已经走过的路径啦。
#include<bits/stdc++.h>
using namespace std;
int n,m;
int tot=0,len=0;
int pd1=-1,pd0=-1;
char a[600001];
struct TR
{
int son[5],fail,sum;
};
TR ac[600001];
void bt(char s[],int len)
{
int now=0;
for(int i=1;i<=len;i++)
{
int x=s[i]-'0';
if(x==1) pd1=1;
if(x==0) pd0=0;
if(ac[now].son[x]==0) ac[now].son[x]=++tot;
now=ac[now].son[x];
}
ac[now].sum=1;
return ;
}
int q[3000001];
void getfail()
{
int st=1,ed=1;ac[0].fail=0;
for(int i=0;i<=1;i++)
{
if(ac[0].son[i]!=0)
{
ac[ac[0].son[i]].fail=0;
q[ed++]=ac[0].son[i];
}
}
while(st!=ed)
{
int now=q[st];
for(int i=0;i<=1;i++)
{
if(ac[now].son[i]!=0)
{
ac[ac[now].son[i]].fail=ac[ac[now].fail].son[i];
q[ed++]=ac[now].son[i];
ac[ac[now].son[i]].sum|=ac[ac[ac[now].son[i]].fail].sum;
}
else ac[now].son[i]=ac[ac[now].fail].son[i];
}
st++;
}
return ;
}
bool v[300001],ll[300001];
void dfs(int x)
{
if(ll[x]) printf("TAK\n"),exit(0);
if(v[x]||ac[x].sum!=0) return ;
v[x]=true;ll[x]=true;
for(int i=0;i<=1;i++)
{
if(ac[x].son[i]) dfs(ac[x].son[i]);
}
ll[x]=false;
return ;
}
int main()
{
memset(ll,false,sizeof(ll));
memset(v,false,sizeof(v));
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%s",a+1);
int len=strlen(a+1);
bt(a,len);
}
getfail();ac[0].sum=0;
dfs(0);
if(pd1!=1||pd0!=0) printf("TAK\n");
else printf("NIE");
return 0;
}
CF1207G Indie Album服了,这一题我搞了两小时不过,做完看起来的话不算难吧。我是铸币。你看对于一个s1+s2的串串,如果啊,就是能与t串匹配的话呢,那一定可以设置一段是s1,一段是s2嘛,所以我们就可以枚举断点,这是第一点咯,然后啊,我们单独匹配是可以算出来的嘛,所以呢合起来的也可以呀,https://www.cnblogs.com/alex-wei/p/CF1202E.html别人的博客?感觉可以然后捏,就是难说。呃呃呃呃感性理解一下吧当我的断点在某一位置的时候,就可以用乘法原理统计断点两端的数量,于是,问题本质上就是要对文本串的每个前缀求出有多少模式串是他的后缀,我们看对字符串如何处理,具体而言就是,我们可以将其翻转,(妙啊),然后呢,对模式串建AC自动机,考虑fail指针指向的是根到当前串的最长后缀。而所求等价于为模式串的后缀的数量,因此通过fail求和即可。嗯嗯就这样啦!
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m;
int tot[3]={0,0,0};
struct TR
{
int son[30],fail,sum;
};
TR ac[300001][3];
void bt(char s[],int len,int k)
{
int now=0;
for(int i=1;i<=len;i++)
{
int x=s[i]-'a'+1;
if(ac[now][k].son[x]==0) ac[now][k].son[x]=++tot[k];
now=ac[now][k].son[x];
}
ac[now][k].sum++;
return ;
}
int q[300001];
void getfail()
{
for(int k=0;k<=1;k++)
{
int st=1,ed=1;
for(int i=1;i<=26;i++)
{
if(ac[0][k].son[i]!=0)
{
ac[ac[0][k].son[i]][k].fail=0;
q[ed++]=ac[0][k].son[i];
}
}
while(st!=ed)
{
int now=q[st];
for(int i=1;i<=26;i++)
{
if(ac[now][k].son[i]!=0)
{
ac[ac[now][k].son[i]][k].fail=ac[ac[now][k].fail][k].son[i];
ac[ac[now][k].son[i]][k].sum+=ac[ac[ac[now][k].son[i]][k].fail][k].sum;
q[ed++]=ac[now][k].son[i];
}
else ac[now][k].son[i]=ac[ac[now][k].fail][k].son[i];
}
st++;
}
}
return ;
}
int res[200001][3];
void find(char s[],int len,int k)
{
int now=0;
for(int i=1;i<=len;i++)
{
int x=s[i]-'a'+1;
now=ac[now][k].son[x];
res[i][k]=ac[now][k].sum;
}
return ;
}
char a1[200001],t[200001],a2[200001],tt[200001];
signed main()
{
scanf("%s%lld",t+1,&n);
for(int i=1;i<=n;i++)
{
scanf("%s",a1+1);
int len=strlen(a1+1),zz=len;
for(int i=1;i<=len;i++) a2[zz--]=a1[i];
bt(a1,len,0);bt(a2,len,1);
}
getfail();
int len=strlen(t+1),len1=len;
for(int i=1;i<=len;i++) tt[len1--]=t[i];
find(t,len,0);find(tt,len,1);
int ans=0;
for(int i=1;i<len;i++) ans+=res[i][0]*res[len-i][1];//枚举正反串呗
printf("%lld",ans);
return 0;
}
明天就下一题啦:总的来说鸽了很久其实蛮简单的,就不打了吧QWQ,
#include <bits/stdc++.h>
#define rep(i, a, b) for(int i = (a); i <= (b); i++)
#define per(i, a, b) for(int i = (a); i >= (b); i--)
#define pii pair<int,int>
#define mp make_pair
#define ll long long
#define pb push_back
using namespace std;
const int MAX = 1e6+1000;
const int N = 1e6+1000;
const int SIGMA_SIZE = 26;
int ch[MAX][SIGMA_SIZE];
int f[MAX], sz, dfn[MAX], dfn_, out[MAX];
vector<int> to[MAX];
void init() {
dfn_ = 0;
sz = 1; //节点个数
memset(ch, 0, sizeof(ch)); //路径
memset(f, 0, sizeof(f)); //失配指针
}
int creat(char *s) {
int u = 0, len = strlen(s);
for (int i = 0; i < len; i++) {
int c = s[i]-'a';
if (!ch[u][c]) ch[u][c] = sz++;
u = ch[u][c];
}
return u;
}
void get_fail() { //找失配指针
queue<int> q;
for (int i = 0; i < SIGMA_SIZE; i++)
if (ch[0][i]) q.push(ch[0][i]);
while (!q.empty()) {
int r = q.front();
q.pop();
for (int c = 0; c < SIGMA_SIZE; c++) {
int u = ch[r][c];
if (!u) {
ch[r][c] = ch[f[r]][c];
continue;
}
q.push(u);
int v = f[r];
while (v && ch[v][c] == 0) v = f[v];
f[u] = ch[v][c];
}
}
}
void Dfs(int u) {
dfn[u] = ++dfn_;
for(auto v:to[u])
Dfs(v);
out[u] = dfn_;
}
//---------------------------
int tree[N];
ll lb(ll x) {return x&-x;}
void add(ll pos, ll data) {
for(int i = pos; i <= N-10; i += lb(i)) {
tree[i] += data;
}
}
ll query(ll pos) {
ll ans = 0;
for(int i = pos; i > 0 ; i -= lb(i)) {
ans += tree[i];
}
return ans;
}
//----------------------
char str[N];
int ans[N];
int nxt[N][30];
int id[N],cnt;
vector<int> from[N];
int put(int &x) {
if(x==-1) {
cnt++;
x = cnt;
}
return x;
}
vector<pii> que[N];
void dfs(int u,int sta) {
add(dfn[sta],1);
for(auto x:from[u])
for(auto y:que[x])
ans[y.second] = query(out[y.first])-query(dfn[y.first]-1);
rep(i, 0, 25) {
int v = nxt[u][i];
if(v==-1) continue;
dfs(v,ch[sta][i]);
add(dfn[ch[sta][i]],-1);
}
}
int main() {
//freopen("a.txt","r",stdin);
ios::sync_with_stdio(false);
int n,m;
memset(nxt,-1,sizeof(nxt));
cin>>n;
rep(i, 1, n) {
int oper;
cin>>oper;
if(oper==1) {
char a;
cin>>a;
id[i] = put(nxt[0][a-'a']);
from[id[i]].pb(i);
}
else {
int u;
char a;
cin>>u>>a;
id[i] = put(nxt[id[u]][a-'a']);
from[id[i]].pb(i);
}
}
cin>>m;
init();
rep(i, 1 ,m) {
int u;
cin>>u>>str;
que[u].pb(mp(creat(str),i));
}
get_fail();
rep(i, 1, sz-1) to[f[i]].pb(i);
Dfs(0);
dfs(0,0);
rep(i, 1, m) cout<<ans[i]<<endl;
}