AC自动机学习笔记
AC自动机经常有人说是在字典树上看毛片 KMP,那么,到底是什么呢?我们仔细来看看吧!!!假装有背景,有气势。。。
先庆祝一波,作为一本通的最后一个字符串里的最后一章节,当然,没有后缀数组QAQ,当然也暂时不打算去学。。。
AC机简介
AC自动机?能够自动AC题目的算法?那岂不无敌,干脆叫AK自动机算了。。。不不不。AC自动机是Aho-Corasick automation发现的,该算法在1975年产生于贝尔实验室,是著名的多模式匹配算法之一。
其实本质是在Trie上跑Kmp,然后加一些优化,使他跑得飞快。
算法原理
先将所有的模式串插入Trie。
这是前提。
然后提一下,在这里的fail指针跟kmp数组的定义不太一样,他定义的是以当前节点为终点的后缀最大能跟那个模式串的前缀相等,同时记录这个节点。
黄色就为fail指针所指向的节点。
如何构造?
我们发现,每个节点指向的都是层数比自己小的节点,我们可以用BFS遍历,一步一步处理层数越来越小的。
- 将0号节点的儿子加入list,然后设他的fail指针为0。
- 开始BFS,取出队头,循环一遍儿子节点,设\(now=fail_{当前的节点},c=儿子节点的字符\),判断now是否有字符为c的儿子,没有,\(now=fail_{now}\),不断下去,知道\(now==0\)或者有这个儿子的时候,儿子节点的fail就等于\(son[now]_{c}\),同时将儿子节点加入队列。
然后,匹配母串的时候,我们只需要不断的跳fail也就可以实现。
现在不多说,看不懂也没关系作者语文不好没办法,后面的优化会让你更加清楚与明白。
优化与实现
算法原理大家基本上已经很清晰了QMQ,语文不好QAQ但是这样跑起来会慢,更何况AC机是要解决多模式匹配的,慢一点全局都会崩端!
尤其是慢慢跳KMP的那个方法,很容易被卡,回顾一下我们是如何匹配的?
我们发现当前的now没有b这个儿子。
于是我们继续跳:
我们发现有b这个儿子,并让队首的儿子的fail等于了now的b儿子。
真棒棒!
但是大佬不满足现状,做了个大胆的假设:
如果在这一步的时候就匹配好了,就等于一次就匹配完毕了!
于是,我们考虑在将now取出队列的时候,当我们发现now没有这个儿子的时候,就让now的这个儿子代表\(fail_{now}\)的儿子。
于是,我们得到了一个图(全部匹配完):
绿色的代表这个节点没有这个儿子而去指向别人的儿子。
我们发现有个a自己指向了自己?因为他的fail指针是根节点,根节点的a儿子就是他自己,会不会有错误?不会,同时,字典树变成了字典图!
因此,我们得到每个节点的儿子的定义,代表跳到当前节点的这个儿子,如果原本没有,则是代表在不断跳fail的过程中,第一个有这个儿子的节点的这个儿子的编号,至于为什么正确,很好想,就不浪费章节了作者是真的懒。。。。
给出BFS过程:
int list[N],head,tail;
void bfs()
{
head=1;tail=1;//初始化
for(int i=0;i<=25;i++)
{
if(tr[0].a[i])list[tail++]=tr[0].a[i];//将0号节点的儿子塞入队列
}
while(head!=tail)
{
int x=list[head++];//取队头
for(int i=0;i<=25;i++)
{
int y=tr[x].a[i];//找儿子
if(y)//有儿子,为他匹配fail指针
{
int p=tr[tr[x].f].a[i];tr[y].f=p;//匹配fail指针
// if(tr[p].v)tr[y].last=p;
// else tr[y].last=tr[p].last;
list[tail++]=y;//加入队列
}
else tr[x].a[i]=tr[tr[x].f].a[i];//没有儿子,指向fail指针所指向节点的儿子
}
}
}
匹配母串也差不多,不再赘述。
也许有人问了,中间注释掉的两行是什么?
接下来讲的优化:last优化。
在例题中,我们要统计出现的单词数量,但是我们在匹配由于是目前匹配的最长长度,但有可能有的串已经出现了,但不是最长长度。
如图:
我们匹配到now的位置,并且答案++,但是我们发现ab的串也匹配成功了!于是我们需要不断的从当前匹配节点跳fail来加上所有已经匹配到的串。
这样是可以AC简单版的。
但是:
加强版。。。
给出题人寄刀片吧。。。
我们为何要不断的跳fail指针呢?快呀!
我们可以用last来记录最长的与这个节点的后缀相等的字符串的最后一个节点,我们只需要跳last就可以保证不会跳多余的次数了。
//简单版代码
//双倍经验!(https://loj.ac/problem/10057)
#include<cstdio>
#include<cstring>
#define N 1100000
using namespace std;
struct trie
{
int a[26],v,f,last;//v是以他为结尾的有多少字符串,f是fail指针,last是个优化
}tr[N];int trlen,n;
char st[N];
void add()//字典树添加
{
int len=strlen(st+1),root=0;
for(int i=1;i<=len;i++)
{
int k=st[i]-'a';
if(!tr[root].a[k])tr[root].a[k]=++trlen;
root=tr[root].a[k];
}
tr[root].v++;
}
int list[N],head,tail;
void bfs()
{
head=1;tail=1;//初始化
for(int i=0;i<=25;i++)
{
if(tr[0].a[i])list[tail++]=tr[0].a[i];//将0号节点的儿子塞入队列
}
while(head!=tail)
{
int x=list[head++];//取队头
for(int i=0;i<=25;i++)
{
int y=tr[x].a[i];//找儿子
if(y)//有儿子,为他匹配fail指针
{
int p=tr[tr[x].f].a[i];tr[y].f=p;//匹配fail指针
if(tr[p].v)tr[y].last=p;
else tr[y].last=tr[p].last;//匹配last
list[tail++]=y;//加入队列
}
else tr[x].a[i]=tr[tr[x].f].a[i];//没有儿子,指向fail指针所指向节点的儿子
}
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%s",st+1);
add();
}
bfs();//AC机
scanf("%s",st+1);
int root=0,ans=0,m=strlen(st+1);
for(int i=1;i<=m;i++)
{
int k=st[i]-'a';
root=tr[root].a[k];//看看目前匹配到的节点是哪个?
int up=root;//统计答案
while(up)
{
if(tr[up].v==-1)break;//如果这个节点被走过,代表前面的都走过,就不走了
ans+=tr[up].v;tr[up].v=-1;//标记与统计
up=tr[up].last;//继续跳
}
}
printf("%d\n",ans);//输出。
return 0;
}
//加强版代码
//注意,没有重复字符串
#include<cstdio>
#include<cstring>
using namespace std;
struct trie
{
int a[26],v,last,f;
}tr[11000];int trlen;
int bk[210];
char st[210][110];
char stc[1100000];
int n,m;
void add(int id)
{
int len=strlen(st[id]+1),root=0;
for(int i=1;i<=len;i++)
{
int k=st[id][i]-'a';
if(!tr[root].a[k])tr[root].a[k]=++trlen;
root=tr[root].a[k];
}
if(!tr[root].v)tr[root].v=id;
}
int list[11000],head,tail;
void bfs()//AC自动机板子
{
head=tail=1;
for(int i=0;i<=25;i++)
{
if(tr[0].a[i])list[tail++]=tr[0].a[i];
}
while(head!=tail)
{
int x=list[head++];
for(int i=0;i<=25;i++)
{
int y=tr[x].a[i];
if(y)
{
int p=tr[tr[x].f].a[i];tr[y].f=p;
if(tr[p].v)tr[y].last=p;
else tr[y].last=tr[p].last;
list[tail++]=y;
}
else tr[x].a[i]=tr[tr[x].f].a[i];
}
}
}
inline int mymax(int x,int y){return bk[x]>bk[y]?x:y;}//取匹配数最大字符串编号
int main()
{
while(1)
{
memset(bk,0,sizeof(bk));
memset(tr,0,sizeof(tr));trlen=0;//初始化
scanf("%d",&n);
if(n==0)break;//退出
for(int i=1;i<=n;i++)
{
scanf("%s",st[i]+1);
add(i);//添加
}
bfs();//AC机
scanf("%s",stc+1);m=strlen(stc+1);
int root=0;
for(int i=1;i<=m;i++)
{
int k=stc[i]-'a';root=tr[root].a[k];//匹配
int up=root;
while(up)bk[tr[up].v]++,up=tr[up].last;//统计
}
int maxid=1;
for(int i=2;i<=n;i++)maxid=mymax(maxid,i);//找最大
printf("%d\n",bk[maxid]);
for(int i=1;i<=n;i++)
{
if(bk[i]==bk[maxid])printf("%s\n",st[i]+1);//输出字符串
}
}
return 0;
}
练习
1
一本通例题一跟luogu简单版是双倍经验。。。
记录每个节点在字典树中的father,然后再记录每个字符串结束位置的编号以及长度,然后在匹配过程中标记匹配到的节点,然后最后找一下,就行了。
#include<cstdio>
#include<cstring>
#define N 21000000
#define M 110000
using namespace std;
struct node
{
int fa,a[4],f;//四个儿子,f是fail指针,fa是father
bool bk;
}tr[N];int trlen,gtk[M],glen[M];
char stc[11000000],st[110];
int n,m;
inline int calc(char ch)//计算
{
if(ch=='E')return 0;
else if(ch=='S')return 1;
else if(ch=='W')return 2;
else return 3;
}
void add(int id)//添加第id个字符串
{
int root=0;glen[id]=strlen(st+1);
for(int i=1;i<=glen[id];i++)
{
int k=calc(st[i]);
if(!tr[root].a[k])tr[root].a[k]=++trlen,tr[trlen].fa=root;
root=tr[root].a[k];
}
gtk[id]=root;//记录结束节点
}
int list[N],head,tail;
void bfs()
{
head=tail=1;
for(int i=0;i<=3;i++)
{
if(tr[0].a[i])list[tail++]=tr[0].a[i];
}
while(head!=tail)
{
int x=list[head++];
for(int i=0;i<=3;i++)
{
int &y=tr[x].a[i],p=tr[tr[x].f].a[i];
if(y)
{
tr[y].f=p;
list[tail++]=y;
//这道题不用last
}
else y=p;
}
}
}
int main()
{
scanf("%d%d",&n,&m);
scanf("%s",stc+1);
for(int i=1;i<=m;i++)
{
scanf("%s",st+1);
add(i);
}
bfs();//AC机准备
int root=0;
for(int i=1;i<=n;i++)
{
root=tr[root].a[calc(stc[i])];//匹配
int up=root;
while(up)
{
if(tr[up].bk)break;//优化
tr[up].bk=true;up=tr[up].f;//标记路过节点
}
}
for(int i=1;i<=m;i++)
{
int zuxian=gtk[i],cnt=0;
while(zuxian)//寻找最后的被标记过的节点
{
if(tr[zuxian].bk)break;
cnt++;zuxian=tr[zuxian].fa;
}
printf("%d\n",glen[i]-cnt);//输出
}
return 0;
}
2
这道题与kmp的一道题很像,开个栈,由于不存在一个字符串是另一个字符串的子串,所以last没用。
#include<cstdio>
#include<cstring>
using namespace std;
struct trie
{
int a[26],f,v;//f是fail,v记录字符串长度
}tr[110000];int trlen;//字典树
char st[110000],stc[110000];
void add()
{
int len=strlen(st+1),root=0;
for(int i=1;i<=len;i++)
{
int k=st[i]-'a';
if(!tr[root].a[k])tr[root].a[k]=++trlen;
root=tr[root].a[k];
}
tr[root].v=len;//记录
}
int list[110000],head,tail;
void bfs()//AC机板子
{
head=tail=1;
for(int i=0;i<=25;i++)
{
if(tr[0].a[i])list[tail++]=tr[0].a[i];
}
while(head!=tail)
{
int x=list[head++];
for(int i=0;i<=25;i++)
{
int &y=tr[x].a[i],p=tr[tr[x].f].a[i];
if(y)
{
tr[y].f=p;
list[tail++]=y;
//不用last
}
else y=p;
}
}
}
char zhanss[110000];//栈
int zhan[110000]/*记录栈中匹配到哪个节点*/,zhlen/*栈长度*/,m,n;
int main()
{
scanf("%s",stc+1);m=strlen(stc+1);
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%s",st+1);
add();
}
bfs();
for(int i=1;i<=m;i++)
{
int root=zhan[zhlen];
root=tr[root].a[stc[i]-'a'];//匹配
zhan[++zhlen]=root;zhanss[zhlen]=stc[i];//入栈
if(tr[root].v)zhlen-=tr[root].v;//出栈
}
zhanss[zhlen+1]='\0';
printf("%s\n",zhanss+1);//输出
return 0;
}
3
当时我竟然还打算重构树?其实不用。
记录每个节点被经过的次数,然后在AC机的过程中,每个节点要把自己的附加权值加到自己fail指针指向的节点,当然,要先从层数高的加到层数低的。
为什么正确?当一个节点能直接或间接指向这个字符串,那么就代表答案++。
#include<cstdio>
#include<cstring>
using namespace std;
struct trie
{
int a[26],f,v;
}tr[1100000];int trlen,gtk[210],n;
char st[1100000];
void add(int id)//添加
{
int len=strlen(st+1),root=0;
for(int i=1;i<=len;i++)
{
int k=st[i]-'a';
if(!tr[root].a[k])tr[root].a[k]=++trlen;
root=tr[root].a[k];tr[root].v++;
}
gtk[id]=root;//记录最后一个节点。
}
int list[1100000],head,tail;
void bfs()
{
head=tail=1;
for(int i=0;i<=25;i++)
{
if(tr[0].a[i])list[tail++]=tr[0].a[i];
}
while(head!=tail)
{
int x=list[head++];
for(int i=0;i<=25;i++)
{
int &y=tr[x].a[i],p=tr[tr[x].f].a[i];
if(y)
{
tr[y].f=p;
list[tail++]=y;
}
else y=p;
}
}
//AC自动机匹配。
for(int i=tail-1;i>=1;i--)tr[tr[list[i]].f].v+=tr[list[i]].v;//添加。
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%s",st+1);
add(i);//添加
}
bfs();//AC自动机
for(int i=1;i<=n;i++)printf("%d\n",tr[gtk[i]].v);//输出
return 0;
}
4
用AC机额外记录Trie中每个节点所形成的字符串能够包含那些节点,用二进制储存,然后BFS一遍,最先凑够二进制的字符串就是合格的字符串。
由于要求最小,所以字符串中所有子串都对应在Trie上(简单来说就是BFS是正确的)。
#include<cstdio>
#include<cstring>
#include<cstdlib>
using namespace std;
struct trie
{
int a[26],f,v;
}tr[1100];int trlen,n;//字典树
char st[110];
void add(int id)
{
int len=strlen(st+1),root=0;
for(int i=1;i<=len;i++)
{
int k=st[i]-'A';
if(!tr[root].a[k])tr[root].a[k]=++trlen;
root=tr[root].a[k];
}
tr[root].v|=(1<<(id-1));
}
int list[2100],head,tail;
void bfs()
{
head=tail=1;
for(int i=0;i<=25;i++)
{
if(tr[0].a[i])list[tail++]=tr[0].a[i];
}
while(head!=tail)
{
int x=list[head++];
for(int i=0;i<=25;i++)
{
int &y=tr[x].a[i],p=tr[tr[x].f].a[i];
if(y)tr[y].f=p,tr[y].v|=tr[p].v,list[tail++]=y;//更新。
else y=p;
}
}
}
struct node
{
char c;
int next,sta,id;
}lis[2100000];bool bol[1100][5100];
char ans[2100000];
void find()
{
head=0;tail=1;//循环利用
while(head!=tail)
{
node x=lis[head++];//取队首
for(int i=0;i<=25;i++)
{
if(tr[x.id].a[i])
{
node y;y.id=tr[x.id].a[i];//继续往下走
if(!bol[y.id][tr[y.id].v|x.sta])//判重
{
bol[y.id][tr[y.id].v|x.sta]=true;
y.c=i+'A';y.sta=tr[y.id].v|x.sta;y.next=head-1;//更新
lis[tail++]=y;
if(y.sta==(1<<(n))-1)//找到了
{
int len=0;
for(int j=tail-1;j;j=lis[j].next)ans[++len]=lis[j].c;
for(int j=len;j>=1;j--)printf("%c",ans[j]);
printf("\n");
exit(0);//直接退出。
}
}
}
}
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%s",st+1);
add(i);
}
bfs();//AC机
find();//BFS
return 0;
}
5
这道题,首先,我们不能经过危险节点,危险节点就是不断跳fail能找到一个是一个字符串结尾的节点的节点。
然后DFS一遍找环,两个BOOL数组,代表是否走过与当前是否在栈里面,当我们DFS找到了在栈里面的节点,就代表找到了,退出。
注意:这里找到了如果不再栈内的话但是走过的话,不算找到,我就被坑了QAQ。
一个错误的例子:
代码:
#include<cstdio>
#include<cstring>
#include<cstdlib>
using namespace std;
struct trie
{
int a[26],f,v;
}tr[1100];int trlen,n;//字典树
char st[110];
void add(int id)
{
int len=strlen(st+1),root=0;
for(int i=1;i<=len;i++)
{
int k=st[i]-'A';
if(!tr[root].a[k])tr[root].a[k]=++trlen;
root=tr[root].a[k];
}
tr[root].v|=(1<<(id-1));
}
int list[2100],head,tail;
void bfs()
{
head=tail=1;
for(int i=0;i<=25;i++)
{
if(tr[0].a[i])list[tail++]=tr[0].a[i];
}
while(head!=tail)
{
int x=list[head++];
for(int i=0;i<=25;i++)
{
int &y=tr[x].a[i],p=tr[tr[x].f].a[i];
if(y)tr[y].f=p,tr[y].v|=tr[p].v,list[tail++]=y;//更新。
else y=p;
}
}
}
struct node
{
char c;
int next,sta,id;
}lis[2100000];bool bol[1100][5100];
char ans[2100000];
void find()
{
head=0;tail=1;//循环利用
while(head!=tail)
{
node x=lis[head++];//取队首
for(int i=0;i<=25;i++)
{
if(tr[x.id].a[i])
{
node y;y.id=tr[x.id].a[i];//继续往下走
if(!bol[y.id][tr[y.id].v|x.sta])//判重
{
bol[y.id][tr[y.id].v|x.sta]=true;
y.c=i+'A';y.sta=tr[y.id].v|x.sta;y.next=head-1;//更新
lis[tail++]=y;
if(y.sta==(1<<(n))-1)//找到了
{
int len=0;
for(int j=tail-1;j;j=lis[j].next)ans[++len]=lis[j].c;
for(int j=len;j>=1;j--)printf("%c",ans[j]);
printf("\n");
exit(0);//直接退出。
}
}
}
}
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%s",st+1);
add(i);
}
bfs();//AC机
find();//BFS
return 0;
}
6
这道题,先建AC自动机的图,然后DP,\(f[i][j]\)代表在字典图中最后一位在\(i\)号节点所建的最长的字符串的数量(不包含一个危险节点,定义与上一道题一样),然后DP转移,用容斥原理容易知道,只要总方案数减去没有包含一个模式串的方案数就行了。
#include<cstdio>
#include<cstring>
#define MOD 10007
using namespace std;
struct trie
{
int a[26],f;
bool v;
}tr[61000];int trlen,n,m;//字典树
char st[110];
void add()
{
int len=strlen(st+1),root=0;
for(int i=1;i<=len;i++)
{
int k=st[i]-'A';
if(!tr[root].a[k])tr[root].a[k]=++trlen;
root=tr[root].a[k];
}
tr[root].v=1;
}//添加
int list[61000],head,tail;
void bfs()
{
head=tail=1;
for(int i=0;i<=25;i++)
{
if(tr[0].a[i])list[tail++]=tr[0].a[i];
}
while(head!=tail)
{
int x=list[head++];
for(int i=0;i<=25;i++)
{
int &y=tr[x].a[i],p=tr[tr[x].f].a[i];
if(y)tr[y].f=p,tr[y].v|=tr[p].v,list[tail++]=y;//找到所有的危险节点
else y=p;
}
}
}//构建AC自动机
int dp[61000][110];
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%s",st+1);
add();
}
bfs();
dp[0][0]=1;//初始化
for(int k=1;k<=m;k++)
{
for(int i=0;i<=trlen;i++)
{
for(int j=0;j<=25;j++)
{
int y=tr[i].a[j];
if(!tr[y].v)dp[y][k]+=dp[i][k-1],dp[y][k]%=MOD;
}
}
}//DP方程
int ans1=1;
for(int i=1;i<=m;i++)ans1*=26,ans1%=MOD;
int ans2=0;
for(int i=0;i<=trlen;i++)ans2+=dp[i][m],ans2%=MOD;
printf("%d\n",(ans1-ans2+MOD)%MOD);//统计与相减
return 0;
}
7
暴力每个串匹配,当y的字符串上的某个节点能够直接或间接调到x字符串的结尾,就代表可以,答案++,当然TLE也十分的多QAQ。
暴力建个桶,同时离线将每个询问按y排序,然后每次跳fail的时候,问一下当前节点是否在桶里面,但是空间与时间都很棒棒!
注:一个小优化,其实可以不跳fail,可以直接跳last的,没试过,理论上可以。
那么,我们继续考虑,跳fail?我们发现一个节点的fail只会指向层数比自己低的节点,同时fail只有一个?我们可以拿trie的节点重构一棵树,当树上yy是xx的儿子,仅当yy的fail指向xx。
然后我们遍历一遍字典树,将他在重构的树中的点权设为1,然后当遇到一个字符串的结尾节点,就统计以当前字符串编号为y的所有询问中x的字符串的结尾节点在重构树中的子树权值和。
当然,还不够快,我们可以发现子树的DFS的编号是连续的,然后我们可以处理出重构树中的每个节点的DFS的编号与子树中DFS编号最大的编号是哪个,用树状数组维护每个编号的权值,统计子树和。
还是会T三个点。
然后我们可以在建Trie时还可以有一些优化,统计一下空间,每次加一个字符,由于Trie的特性,顶多加一个节点,那么Trie中的节点数最多为100000。
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 210000
#define NN 110000
using namespace std;
struct trie
{
int a[26],f,v,fa;//fa就是father,主要用来判断他是原本字典树的还是后来字典图的一种方法
}tr[N];int trlen,cnt,trss[N];
int list[N],head,tail;
void bfs()
{
head=tail=1;
for(int i=0;i<=25;i++)
{
if(tr[0].a[i])list[tail++]=tr[0].a[i];
}
while(head!=tail)
{
int x=list[head++];
for(int i=0;i<=25;i++)
{
int &y=tr[x].a[i],p=tr[tr[x].f].a[i];
if(y)
{
tr[y].f=p;
list[tail++]=y;
}
else y=p;
}
}
}
//AC机模版
struct node
{
int y,next;
}a[N];int last[N],alen;
inline void ins(int x,int y)
{
alen++;
a[alen].y=y;a[alen].next=last[x];last[x]=alen;
}
//边目录,重构树
struct answers
{
int x,y,id;
}q[NN];/*离线操作*/int n,m,bk[NN]/*答案*/,ql[NN],qr[NN];
int dfn[NN]/*当前节点的字典序*/,low[NN]/*当前子树最大的DFS序*/,times/*时间戳*/;
inline bool cmp(answers x,answers y){return x.y<y.y;}
inline void dfs(int x)
{
dfn[x]=++times;
for(int k=last[x];k;k=a[k].next)dfs(a[k].y);
low[x]=times;
}
//处理字典序。
int bit[N];
inline int lowbit(int x){return x&(-x);}
inline void change(int x,int p){while(x<=times)bit[x]+=p,x+=lowbit(x);}
inline int getsum(int x)
{
int ans=0;
while(x)ans+=bit[x],x-=lowbit(x);
return ans;
}
//树状数组
void find(int x)//遍历字典树
{
change(dfn[x],1);
if(tr[x].v && ql[tr[x].v]!=0)
{
int y=tr[x].v;
for(int i=ql[y];i<=qr[y];i++)//遍历y是的tr[x].v的询问
{
bk[q[i].id]+=getsum(low[trss[q[i].x]])-getsum(dfn[trss[q[i].x]]-1);//统计
}
}
for(int i=0;i<=25;i++)
{
if(tr[x].a[i] && tr[tr[x].a[i]].fa==x)find(tr[x].a[i]);//继续遍历
}
change(dfn[x],-1);
}
char st[NN];
int main()
{
scanf("%s",st+1);m=strlen(st+1);
int root=0;
for(int i=1;i<=m;i++)
{
if(st[i]=='B')root=tr[root].fa;
else if(st[i]=='P')tr[root].v=++cnt,trss[cnt]=root;
else
{
int k=st[i]-'a';
if(!tr[root].a[k])tr[root].a[k]=++trlen,tr[trlen].fa=root;
root=tr[root].a[k];
}
}//省去了添加中寻找的时间,直接在主函数中快速解决,快了3000ms!
scanf("%d",&n);
for(int i=1;i<=n;i++){scanf("%d%d",&q[i].x,&q[i].y);q[i].id=i;}//离线
sort(q+1,q+n+1,cmp);//排序
n++;
for(int i=1;i<=n;i++)
{
if(q[i].y!=q[i-1].y)ql[q[i].y]=i,qr[q[i-1].y]=i-1;//为找答案做准备
}
n--;ql[0]=qr[0]=0;
bfs();//AC机
for(int i=1;i<=trlen;i++)ins(tr[i].f,i);//重构树
dfs(0);//字典序。
find(0);//寻找
for(int i=1;i<=n;i++)printf("%d\n",bk[i]);//输出
return 0;
}
8
【题意】
给出有一个L*C的字符地图,地图的行与列都从0开始编号
然后给出一些字符串,求出这些字符串在字符地图上的位置。(数据保证有唯一解)
输出字符串第一个字母的坐标和字符串的方向
字符串的方向是指字符串的走向
A表示正北,B表示东北,C表示正东,D表示东南,E表示正南,F表示西南,G表示正西,H表示西北
且保证字符串的方向是固定的
【输入格式】
第一行输入L,C,W(0<L,C,W<=1000)
L表示行数,C表示列数,W表示字符串的数量
然后输入L*C的字符矩阵
最后输入W行字符串(字符串长度<=1000)
【输出格式】
输出W行,每行对应第i个字符串第一个字母的坐标和字符串的方向
【样例输入】
20 20 10
QWSPILAATIRAGRAMYKEI
AGTRCLQAXLPOIJLFVBUQ
TQTKAZXVMRWALEMAPKCW
LIEACNKAZXKPOTPIZCEO
FGKLSTCBTROPICALBLBC
JEWHJEEWSMLPOEKORORA
LUPQWRNJOAAGJKMUSJAE
KRQEIOLOAOQPRTVILCBZ
QOPUCAJSPPOUTMTSLPSF
LPOUYTRFGMMLKIUISXSW
WAHCPOIYTGAKLMNAHBVA
EIAKHPLBGSMCLOGNGJML
LDTIKENVCSWQAZUAOEAL
HOPLPGEJKMNUTIIORMNC
LOIUFTGSQACAXMOPBEIO
QOASDHOPEPNBUYUYOBXB
IONIAELOJHSWASMOUTRK
HPOIYTJPLNAQWDRIBITG
LPOINUYMRTEMPTMLMNBO
PAFCOPLHAVAIANALBPFS
MARGARITA
ALEMA
BARBECUE
TROPICAL
SUPREMA
LOUISIANA
CHEESEHAM
EUROPA
HAVAIANA
CAMPONESA
【样例输出】
0 15 G
2 11 C
7 18 A
4 8 C
16 13 B
4 15 E
10 3 D
5 1 E
19 7 C
11 11 H
真的想打出题人,思路很简单,就是恶心。
我们可以给要匹配的串建一个AC机,然后拿地图跑一跑,因为同个方向不同的出发点(不在一个方向上)是不会有相交的路径的。所以每个方向我们都可以跑地图进行更新。
#include<cstdio>
#include<cstring>
#define N 1100
#define M 1100000
using namespace std;
int fa[N];
int findfa(int x)
{
if(fa[x]!=x)return fa[x]=findfa(fa[x]);
else return x;
}//相同的串的答案我们用并查集维护
struct node
{
int a[30],c,last,f;
}tr[M];int len;
struct answer
{
int x,y;char z;
int len;
}ans[N];
char st[N];int slen;
void ins(int id)
{
int x=0;
for(int i=1;i<=slen;i++)
{
if(!tr[x].a[st[i]-'A'])tr[x].a[st[i]-'A']=++len;
x=tr[x].a[st[i]-'A'];
}
if(tr[x].c)fa[tr[x].c]=id;
tr[x].c=id;
}
int list[M],head,tail;
void bfs()
{
head=1;tail=0;
for(int i=0;i<=25;i++)
{
if(tr[0].a[i])list[++tail]=tr[0].a[i];
}
while(head<=tail)
{
int x=list[head++];
for(int i=0;i<=25;i++)
{
int y=tr[x].a[i];
if(y)
{
tr[y].f=tr[tr[x].f].a[i];
if(tr[tr[y].f].c)tr[y].last=tr[y].f;
else tr[y].last=tr[tr[y].f].last;
list[++tail]=y;
}
else tr[x].a[i]=tr[tr[x].f].a[i];
}
}
}//AC机
int n,m,T;
char mp[N][N];
struct question
{
int x,y;
}op[N];int olen;
inline void change(int id,int tt,char z)
{
tt=tt-ans[id].len+1;//更新到开头的坐标
int x=op[tt].x,y=op[tt].y;
if(ans[id].x>x || (ans[id].x==x && ans[id].y>y))ans[id].x=x,ans[id].y=y,ans[id].z=z;//可以更新答案
}//更新答案
void chuli(char qaq)
{
int x=0;
for(int i=1;i<=olen;i++)
{
x=tr[x].a[mp[op[i].x][op[i].y]-'A'];
if(tr[x].c)change(tr[x].c,i,qaq);
int z=tr[x].last;
while(z)
{
change(tr[z].c,i,qaq);
z=tr[z].last;
}
}
}
inline int mymin(int x,int y){return x<y?x:y;}
void work()
{
for(int i=1;i<=m;i++)
{
olen=0;
for(int j=n;j>=1;j--)op[++olen].x=j,op[olen].y=i;
chuli('A');
}
//北
for(int i=1;i<=n;i++)
{
olen=0;
int ed=mymin(i,m);
for(int j=1;j<=ed;j++)op[++olen].x=i-j+1,op[olen].y=j;
chuli('B');
}
for(int i=2;i<=m;i++)
{
olen=0;
int ed=mymin(m-i+1,n);
for(int j=1;j<=ed;j++)op[++olen].x=n-j+1,op[olen].y=i+j-1;
chuli('B');
}
//东北
for(int i=1;i<=n;i++)
{
olen=0;
for(int j=1;j<=m;j++)op[++olen].x=i,op[olen].y=j;
chuli('C');
}
//东
for(int i=1;i<=n;i++)
{
olen=0;
int ed=mymin(n-i+1,m);
for(int j=1;j<=ed;j++)op[++olen].x=i+j-1,op[olen].y=j;
chuli('D');
}
for(int i=2;i<=m;i++)
{
olen=0;
int ed=mymin(m-i+1,n);
for(int j=1;j<=ed;j++)op[++olen].x=j,op[olen].y=i+j-1;
chuli('D');
}
//东南
for(int i=1;i<=m;i++)
{
olen=0;
for(int j=1;j<=n;j++)op[++olen].x=j,op[olen].y=i;
chuli('E');
}
//南
for(int i=1;i<=n;i++)
{
olen=0;
int ed=mymin(n-i+1,m);
for(int j=1;j<=ed;j++)op[++olen].x=i+j-1,op[olen].y=m-j+1;
chuli('F');
}
for(int i=1;i<m;i++)
{
olen=0;
int ed=mymin(i,n);
for(int j=1;j<=ed;j++)op[++olen].x=j,op[olen].y=i-j+1;
chuli('F');
}
//西南
for(int i=1;i<=n;i++)
{
olen=0;
for(int j=m;j>=1;j--)op[++olen].x=i,op[olen].y=j;
chuli('G');
}
//西
for(int i=1;i<=n;i++)
{
olen=0;
int ed=mymin(i,m);
for(int j=1;j<=ed;j++)op[++olen].x=i-j+1,op[olen].y=m-j+1;
chuli('H');
}
for(int i=1;i<m;i++)
{
olen=0;
int ed=mymin(i,n);
for(int j=1;j<=ed;j++)op[++olen].x=n-j+1,op[olen].y=i-j+1;
chuli('H');
}
//西南
}
int main()
{
scanf("%d%d%d",&n,&m,&T);
for(int i=1;i<=n;i++)scanf("%s",mp[i]+1);
for(int i=1;i<=T;i++)
{
scanf("%s",st+1);
ans[i].len=slen=strlen(st+1);
ins(i);
fa[i]=i,ans[i].x=ans[i].y=999999999;
}
bfs();
work();
for(int i=1;i<=T;i++)printf("%d %d %c\n",ans[i].x-1,ans[i].y-1,ans[i].z);
return 0;
}
9
【题意】
给出n个模式串,然后给出一个修改串,求尽量少修改修改串,使得修改串不含有任何一个模式串,不能的话输出-1
每个串只有'A','C','G','T'四个字母
【输入格式】
有多组数据,输入以一个0结束
每组数据:
输入一个n(n<=50)
接下来n行输入n个模式串(每个模式串长度不超过20)
最后一行输入修改串(长度不超过1000)
【输出格式】
输出Case T: ans
T当前输出的是第T组数据,ans表示最少修改次数,不能修改则ans=-1
【样例输入】
2
AAA
AAG
AAAG
2
A
TG
TGAATG
4
A
G
C
T
AGT
0
【样例输出】
Case 1: 1
Case 2: 4
Case 3: -1
这道题目我们又该怎么做?我们先建出一棵好看的AC机,然后我们用一个\(dp\)数组,\(dp[i][j]\)表示的是当我们到第\(i\)个字符的时候,在第\(j\)个位置需要多少的修改次数。
需要注意一点的是,不仅单词末尾不能走,\(fail\)指针能到单词末尾的点也不能走。
#include<cstdio>
#include<cstring>
#define N 1100
using namespace std;
int ff[150],n,m;
char st[N];int slen;
struct node
{
int a[4],c,f,dp[N];
}tr[N];int len;
inline int mymin(int x,int y){return x<y?x:y;}
inline void clear(int x){tr[x].a[0]=tr[x].a[1]=tr[x].a[2]=tr[x].a[3]=tr[x].c=tr[x].f=0;}
void ins()
{
int x=0;
for(int i=1;i<=slen;i++)
{
int y=ff[st[i]];
if(!tr[x].a[y])x=tr[x].a[y]=++len,clear(x);
else x=tr[x].a[y];
if(tr[x].c)return ;
}
tr[x].c++;
}
int list[N],head,tail;
void bfs()
{
head=1;tail=0;
for(int i=0;i<=3;i++)
{
if(tr[0].a[i])list[++tail]=tr[0].a[i];
}
while(head<=tail)
{
int x=list[head++];
for(int i=0;i<=3;i++)
{
int y=tr[x].a[i];
if(y)
{
tr[y].f=tr[tr[x].f].a[i];
list[++tail]=y;
if(tr[tr[y].f].c)tr[y].c++;//这一句话很关键
}
else tr[x].a[i]=tr[tr[x].f].a[i];
}
}
}
void dp(int id)
{
for(int i=0;i<=len;i++)
{
for(int j=0;j<=slen;j++)tr[i].dp[j]=999999999;
}
tr[0].dp[0]=0;
for(int i=0;i<slen;i++)
{
for(int j=0;j<=len;j++)
{
if(tr[j].dp[i]!=999999999 && !tr[j].c)
{
for(int k=0;k<=3;k++)
{
int son=tr[j].a[k],add=0;
tr[son].dp[i+1]=mymin(tr[son].dp[i+1],tr[j].dp[i]+(ff[st[i+1]]!=k));
}
}
}
}
int ans=999999999;
for(int i=0;i<=len;i++)
{
if(!tr[i].c)ans=mymin(ans,tr[i].dp[slen]);
}
if(ans==999999999)ans=-1;
printf("Case %d: %d\n",id,ans);
}
int main()
{
// freopen("1.in","r",stdin);
// freopen("1.out","w",stdout);
ff['A']=0;ff['C']=1;ff['G']=2;ff['T']=3;
int cnt=0;
while(scanf("%d",&n)!=EOF)
{
if(n==0)break;
cnt++;len=0;clear(0);
for(int i=1;i<=n;i++)
{
scanf("%s",st+1);
slen=strlen(st+1);
ins();
}
bfs();
scanf("%s",st+1);
slen=strlen(st+1);
dp(cnt);
}
return 0;
}