AC 自动机
重新学 自动机发现以前就像没见过一样……
首先是一段经典的话:“ 自动机是 树上跑 ”
于是 自动机的关键在于运用 进行匹配
由于这时的 形成一棵树形结构,可以将一些匹配问题转化为树上问题
如果 匹配到了文本串,那么所有 的祖先也都是可以匹配上的
那么经常可以通过统计祖先的前缀和来方便地维护
动态题目可以用树状数组等结构来维护子树信息
另外,为了加快失配匹配,可以建立虚儿子,于是匹配可以连续进行
至于具体的图就先不模拟了
一定注意一个大坑点是在这种 图上父亲的特殊标记是要下传到儿子上的
注意父亲的编号不一定比儿子的小,因此这部分递推要写在 里,不能直接 循环!!!
另外补充一点,可以发现 自动机的构建过程复杂度是要乘上字符集大小的,如果字符集较大,我们可以用主席树来构建,因为写出转移会发现大部分儿子都是从 继承过来的
自动机上的
自动机上的 一般都设 表示长度为 的串结束于 节点的情况,那么转移相当于枚举所有的儿子进行移动
还有就是 的过程中尽量通过改变自动机的样子来调整 ,而不是无脑往上加状态
比如要求到达关键串停止,其实没有必要增加状态记录有没有到达关键串,而是直接把自动机上所有关键串的儿子指向自己即可
设 表示长度为 且结束于 节点,卡不卡上限的方案数
那么模拟题意进行转移即可
对于前导零的情况可以初始化时给 进行赋值
不合法的串建立 树时顺便打标记,注意需要把 的标记继承过来
代码
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
const int maxn=1205;
const int maxm=1505;
int n,m,trie[maxm][10],tot,q[maxm],f[maxn][maxm][2],ans,nxt[maxm];
bool vis[maxm];
char a[maxn],b[maxm];
void insert(char* str){
int len=strlen(str+1),p=0;
for(int i=1;i<=len;i++){
int x=str[i]-'0';
if(!trie[p][x])trie[p][x]=++tot;
p=trie[p][x];
}
vis[p]=true;
return ;
}
void build(){
int hd=1,tl=0;
for(int i=0;i<=9;i++){
if(trie[0][i])q[++tl]=trie[0][i];
}
while(hd<=tl){
int p=q[hd++];
for(int i=0;i<=9;i++){
int x=trie[p][i];
if(!x)trie[p][i]=trie[nxt[p]][i];
else{
nxt[x]=trie[nxt[p]][i];
vis[x]|=vis[nxt[x]];
q[++tl]=x;
}
}
}
return ;
}
void modadd(int &a,int b){
a=(a+b)%mod;
return ;
}
int main(){
scanf("%s",a+1);
cin>>m;n=strlen(a+1);
for(int i=1;i<=m;i++)scanf("%s",b+1),insert(b);
build();
for(int i=1;i<a[1]-'0';i++){
if(!vis[trie[0][i]])f[1][trie[0][i]][0]++;
}
if(!vis[trie[0][a[1]-'0']])f[1][trie[0][a[1]-'0']][1]++;
for(int i=2;i<=n;i++){
for(int j=1;j<=9;j++){
if(!vis[trie[0][j]])f[i][trie[0][j]][0]++;
}
}
for(int i=1;i<=n-1;i++){
for(int j=0;j<=tot;j++){
for(int k=0;k<=9;k++)if(!vis[trie[j][k]])modadd(f[i+1][trie[j][k]][0],f[i][j][0]);
for(int k=0;k<a[i+1]-'0';k++)if(!vis[trie[j][k]])modadd(f[i+1][trie[j][k]][0],f[i][j][1]);
if(!vis[trie[j][a[i+1]-'0']])modadd(f[i+1][trie[j][a[i+1]-'0']][1],f[i][j][1]);
}
}
for(int j=0;j<=tot;j++)modadd(ans,(f[n][j][0]+f[n][j][1])%mod);
cout<<ans;
return 0;
}
这个分类讨论就过于阴间了……
直接考虑 的情况,发现从 转移到 或
而在 自动机上的转移方程都是固定的
那么可以把这两行放进矩阵里进行转移
代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=205,mod=1e9+7;
int n,m,l,len[maxn],trie[maxn][26],tot,vis[maxn],ans,nxt[maxn],trans[maxn][maxn];
char s[maxn][maxn],t[maxn];
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;
}
void modadd(int &a,int b){a=(a+b)%mod;}
void insert(char *str){
int len=strlen(str+1),p=0;
for(int i=1;i<=len;i++){
int x=str[i]-'a';
if(!trie[p][x])trie[p][x]=++tot;
p=trie[p][x];
}
vis[p]=true;
}
void build(){
queue<int>q;
for(int i=0;i<26;i++)if(trie[0][i])q.push(trie[0][i]);
while(!q.empty()){
int p=q.front();q.pop();
// cout<<"hhh "<<p<<endl;
for(int i=0;i<26;i++){
if(trie[p][i])nxt[trie[p][i]]=trie[nxt[p]][i],q.push(trie[p][i]);
else trie[p][i]=trie[nxt[p]][i];
}
}
for(int i=0;i<=tot;i++)vis[i]|=vis[nxt[i]];
}
int con(int p,char *str){
int len=strlen(str+1);
for(int i=1;i<=len;i++){
p=trie[p][str[i]-'a'];
if(vis[p])return -1;
}
return p;
}
namespace p1{
int f[maxn][maxn];
void start(){
f[0][0]=1;
for(int i=0;i<=l;i++)for(int j=0;j<=tot;j++)if(f[i][j]){
for(int k=1;k<=n;k++)if(i+len[k]<=l&&~trans[k][j])modadd(f[i+len[k]][trans[k][j]],f[i][j]);
}
for(int i=0;i<=tot;i++)modadd(ans,f[l][i]);
cout<<ans;
}
}
namespace p2{
int all;
struct Mat{
int a[maxn][maxn];
Mat(){memset(a,0,sizeof a);}
}A,B;
Mat operator * (Mat a,Mat b){
Mat c;
for(int i=1;i<=all;i++)for(int j=1;j<=all;j++){
for(int k=1;k<=all;k++)c.a[i][j]+=a.a[i][k]*b.a[k][j]%mod;
c.a[i][j]%=mod;
}
return c;
}
Mat po(Mat a,int b){
Mat ans;for(int i=1;i<=all;i++)ans.a[i][i]=1;
while(b){
if(b&1)ans=ans*a;
a=a*a;
b>>=1;
}
return ans;
}
void print(Mat a){
puts("print out a matrix");
for(int i=1;i<=all;i++,puts(""))for(int j=1;j<=all;j++)cout<<a.a[i][j]<<" ";
puts("");
}
void start(){
tot++;all=tot*2;
for(int i=0;i<tot;i++)if(!vis[i]){
for(int j=1;j<=n;j++)if(~trans[j][i]){
if(len[j]==1)B.a[i+1][trans[j][i]+1]++;
else B.a[i+1][trans[j][i]+tot+1]++;//,B.a[i+1+tot][trans[j][i]+1+tot]++;
// B.a[i+1+(len[j]==1?tot:1)][trans[j][i]+1+tot]++;
}
}
for(int i=0;i<tot;i++)B.a[i+1+tot][i+1]++;
A.a[1][1]=1;
B=po(B,l);
A=A*B;
// print(A);
for(int i=0;i<tot;i++)modadd(ans,A.a[1][i+1]);
cout<<ans;
}
}
signed main(){
n=read(),m=read(),l=read();
for(int i=1;i<=n;i++){
scanf("%s",s[i]+1);
len[i]=strlen(s[i]+1);
}
for(int i=1;i<=m;i++){
scanf("%s",t+1);
insert(t);
}
build();memset(trans,-1,sizeof trans);
for(int i=1;i<=n;i++)for(int j=0;j<=tot;j++)if(!vis[j])trans[i][j]=con(j,s[i]);
if(l<=100)return p1::start(),0;
p2::start();
return 0;
}
这道题 也是一样的道理,居然还是个黑题
结合树上问题
首先插入操作都直接用于构建 自动机即可
对于询问 的本质相当于求 树上 的子树内有多少节点是 串内的
考虑 串在 上走过的路径即是整个串中的节点
那么可以把询问离线,然后遍历整棵 树,而在 树上的子树关系通过树状数组维护 树中的 序来得到
注意遍历 树的时候不能直接用原来的 数组,因为已经经过 自动机的改造变成了一张图,需要备份
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
const int maxm=2e5+5;
#define pi pair<int,int>
#define fi first
#define se second
#define mp make_pair
int n,m,re[maxn],fa[maxn],son[maxn][26],trie[maxn][26],hd[maxn],cnt,q[maxn],x,y,ans[maxn],tot,dfn[maxn],en[maxn],lea[maxn],c[maxn],num,nxt[maxn];
vector<pi>ask[maxn];
char a[maxn];
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;
}
struct Edge{
int nxt,to;
}edge[maxm];
void add(int u,int v){
// cout<<u<<" "<<v<<endl;
edge[++cnt].nxt=hd[u];
edge[cnt].to=v;
hd[u]=cnt;
return ;
}
void insert(){
int p=0;
for(int i=1;i<=n;i++){
if(a[i]=='B')p=fa[p];
else if(a[i]=='P')en[p]=++num,re[num]=p;//,cout<<"hhh "<<p<<" "<<en[p]<<endl;
else{
int x=a[i]-'a';
son[p][x]=trie[p][x]=++tot;
fa[tot]=p;
p=trie[p][x];
}
}
return ;
}
void build(){
int hd=1,tl=0;
for(int i=0;i<26;i++)if(trie[0][i])q[++tl]=trie[0][i],add(0,trie[0][i]);
while(hd<=tl){
int p=q[hd++];
for(int i=0;i<26;i++){
int x=trie[p][i];
if(!x)trie[p][i]=trie[nxt[p]][i];
else{
nxt[x]=trie[nxt[p]][i];
add(nxt[x],x);
q[++tl]=x;
}
}
}
return ;
}
void dfs(int u){
// cout<<u<<" ";
dfn[u]=++tot;
for(int i=hd[u];i;i=edge[i].nxt)dfs(edge[i].to);
lea[u]=++tot;
return ;
}
void add1(int x,int w){
x++;for(;x<=tot+1;x+=x&-x)c[x]+=w;
return ;
}
int ask1(int x){
int res=0;x++;
for(;x;x-=x&-x)res+=c[x];
return res;
}
void dfs1(int u){
// cout<<u<<" ";
add1(dfn[u],1);
for(int i=0;i<ask[u].size();i++){
ans[ask[u][i].se]=ask1(lea[ask[u][i].fi])-ask1(dfn[ask[u][i].fi]-1);
}
for(int i=0;i<26;i++){
if(son[u][i])dfs1(son[u][i]);
}
add1(dfn[u],-1);
return ;
}
int main(){
// freopen("1.in","r",stdin);
// freopen("my.out","w",stdout);
scanf("%s",a+1);n=strlen(a+1);
insert();build();m=read();
for(int i=1;i<=m;i++)x=read(),y=read(),ask[re[y]].push_back(mp(re[x],i));
tot=0;dfs(0);dfs1(0);
for(int i=1;i<=m;i++)printf("%d\n",ans[i]);
return 0;
}
一样的道理,每次在 树上进行单点加和子树和的操作即可
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
const int maxm=1e6+5;
int n,m,hd[maxn],cnt,num,st[maxn],ed[maxn],id[maxn],q[maxn],trie[maxn][26],nxt[maxn],tot,c[maxn],ans;
char b[maxn],op[maxn];
bool vis[maxn];
struct Edge{
int nxt,to;
}edge[maxm];
void add(int u,int v){
// cout<<"hhh "<<u<<" "<<v<<endl;
edge[++cnt].nxt=hd[u];
edge[cnt].to=v;
hd[u]=cnt;
return ;
}
void insert(char* str,int num){
int len=strlen(str+1),p=0;
for(int i=1;i<=len;i++){
int x=str[i]-'a';
if(!trie[p][x])trie[p][x]=++tot;
p=trie[p][x];
}
id[num]=p;
return ;
}
void build(){
int hd=1,tl=0;
for(int i=0;i<26;i++){
if(trie[0][i])q[++tl]=trie[0][i],add(0,trie[0][i]);
}
while(hd<=tl){
int p=q[hd++];
for(int i=0;i<26;i++){
int x=trie[p][i];
if(!x)trie[p][i]=trie[nxt[p]][i];
else{
nxt[x]=trie[nxt[p]][i];
add(nxt[x],x);
q[++tl]=x;
}
}
}
return ;
}
void dfs(int u){
st[u]=++num;
for(int i=hd[u];i;i=edge[i].nxt){
int v=edge[i].to;
dfs(v);
}
ed[u]=num;
return ;
}
int change(char* op){
int len=strlen(op+1),res=0;
for(int i=2;i<=len;i++)res=res*10+op[i]-'0';
return res;
}
void add1(int x,int w){
for(;x<=num;x+=x&-x)c[x]+=w;
return ;
}
int ask1(int x){
int res=0;
for(;x;x-=x&-x)res+=c[x];
return res;
}
void doadd(int l,int r,int w){
// cout<<l<<" "<<r<<endl;
add1(l,w),add1(r+1,-w);return ;
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++)scanf("%s",b+1),insert(b,i);
build();dfs(0);
// for(int i=0;i<=tot;i++)cout<<st[i]<<" "<<ed[i]<<endl;
for(int i=1;i<=m;i++)vis[i]=true,doadd(st[id[i]],ed[id[i]],1);
for(int i=1;i<=n;i++){
scanf("%s",op+1);
if(op[1]=='+'){
int x=change(op);
if(!vis[x]){
vis[x]=true;
doadd(st[id[x]],ed[id[x]],1);
}
}
if(op[1]=='-'){
int x=change(op);
if(vis[x]){
vis[x]=false;
doadd(st[id[x]],ed[id[x]],-1);
}
}
if(op[1]=='?'){
int len=strlen(op+1);ans=0;
for(int i=2,j=0;i<=len;i++){
int x=op[i]-'a';
j=trie[j][x];
ans+=ask1(st[j]);
// cout<<"kkk "<<i<<" "<<j<<" "<<ask1(st[j])<<endl;
}
printf("%d\n",ans);
}
}
return 0;
}
还是一样的套路,只不过变成了改变一个串的权值
一个串的权值在 树上影响的范围仍然是一个区间,那么直接在线段树上开 来维护这个区间的权值集合,查询时拿出最大值,修改时先删后加入
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=3e5+5;
const int maxm=3e5+5;
int n,m,ans,trie[maxn][26],val[maxn],nxt[maxn],q[maxn],hd[maxn],cnt,tot,num,st[maxn],ed[maxn],en[maxn],w,x;
char a[maxn];
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;
}
struct Edge{
int nxt,to;
}edge[maxm];
void add(int u,int v){
// cout<<"hhh "<<u<<" "<<v<<endl;
edge[++cnt].nxt=hd[u];
edge[cnt].to=v;
hd[u]=cnt;
return ;
}
struct Seg{
int l,r;
multiset<int>s;
}t[maxn*4];
void insert(char* str,int id){
int len=strlen(str+1),p=0;
for(int i=1;i<=len;i++){
int x=str[i]-'a';
if(!trie[p][x])trie[p][x]=++tot;
p=trie[p][x];
}
en[id]=p;
return ;
}
void build(){
int hd=1,tl=0;
for(int i=0;i<26;i++)if(trie[0][i])q[++tl]=trie[0][i],add(0,trie[0][i]);
while(hd<=tl){
int p=q[hd++];
for(int i=0;i<26;i++){
int x=trie[p][i];
if(!x)trie[p][i]=trie[nxt[p]][i];
else{
nxt[x]=trie[nxt[p]][i];
add(nxt[x],x);
q[++tl]=x;
}
}
}
return ;
}
void dfs(int u){
st[u]=++num;
for(int i=hd[u];i;i=edge[i].nxt){
int v=edge[i].to;
dfs(v);
}
ed[u]=num;
return ;
}
void build(int p,int l,int r){
t[p].l=l,t[p].r=r;
// if(l==3&&r==3)cout<<" dfasfadf "<<p<<endl;
if(l==r)return ;
int mid=l+r>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
return ;
}
void change(int p,int l,int r,int last,int w,int op){
// if(t[p].l==1&&t[p].r==num)cout<<"change "<<l<<" "<<r<<" "<<last<<" "<<w<<endl;
// cout<<t[p].l<<" "<<t[p].r<<" "<<l<<" "<<r<<endl;
if(t[p].l>=l&&t[p].r<=r){
if(!op){
t[p].s.insert(w);
return ;
}
auto it=t[p].s.find(last);
if(it!=t[p].s.end())t[p].s.erase(it);
t[p].s.insert(w);
return ;
}
int mid=t[p].l+t[p].r>>1;
if(l<=mid)change(p<<1,l,r,last,w,op);
if(r>mid)change(p<<1|1,l,r,last,w,op);
return ;
}
void ask(int p,int pos){
if(!t[p].s.empty())ans=max(ans,*--t[p].s.end());
if(t[p].l==t[p].r)return ;
int mid=t[p].l+t[p].r>>1;
if(pos<=mid)ask(p<<1,pos);
else ask(p<<1|1,pos);
return ;
}
int main(){
n=read(),m=read();
for(int i=1;i<=n;i++)scanf("%s",a+1),insert(a,i);
build();dfs(0);build(1,1,num);
for(int i=1;i<=n;i++){
change(1,st[en[i]],ed[en[i]],0,0,0);
// cout<<"ppp "<<en[i]<<" "<<st[en[i]]<<" "<<ed[en[i]]<<endl;
}
for(int i=1;i<=m;i++){
int op=read();
if(op==1){
x=read(),w=read();
change(1,st[en[x]],ed[en[x]],val[x],w,1);val[x]=w;
}
else{
scanf("%s",a+1);ans=-1;
int len=strlen(a+1);
for(int i=1,j=0;i<=len;i++){
int x=a[i]-'a';
j=trie[j][x];
// cout<<i<<" "<<j<<endl;
ask(1,st[j]);
}
printf("%d\n",ans);
}
/*
cout<<"out: ";
for(auto it=t[5].s.begin();it!=t[5].s.end();it++){
cout<<*it<<" ";
}
puts("");
*/
}
return 0;
}
非常神奇的一个题
首先明确枚举的是母串 ,然后寻找有多少符合条件的子串
对于一个 需要满足的条件其实是有两部分组成:能够作为满足条件 的子串;每次出现时都是满足条件 的
换句话说其在 中出现的次数是等于满足条件 出现的个数
第一个是自动机上用树状数组维护的基本操作,来看第二个怎么求
可以倒序枚举 的每一位,找到一个能匹配后缀的最靠前的串,并且不存在一个之前匹配的串的将其包含
其中最靠左的一个串可以预处理的时候顺便求出
以前写得有点儿抽象,重新来写一发
首先所有备选的串 一定是 树上 代表的每一个节点上的其它字符串(这个结束位置要事先在 图上往下转移一下)
考虑怎样选出这些字符串中满足第三个条件的那些
首先这些串之间不能有包含关系,也就是说如果在串上显式地被包含,就不能加入候选集合了
这个可以从前往后扫的时候维护出当前最靠后的位置,只把在这个位置之前的串加入
于是目前得到了一些互不相交的候选串
但是在这样的情况下仍然有可能被包含,那么我们就用上面提到的出现次数相等来判断
即这个串目前在候选集合中出现的次数=其在 串中出现的总次数即可
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
const int maxm=1e6+5;
const int inf=0x3f3f3f3f;
int n,m,ans,viscnt[maxn],c[maxn],len[maxn],trie[maxn][26],nxt[maxn],q[maxn],hd[maxn],cnt,tot,num,st[maxn],ed[maxn],en[maxn],w,x,endpos[maxn];
vector<int>vis,b[maxn];
char a[maxn];
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;
}
struct Edge{
int nxt,to;
}edge[maxm];
void add(int u,int v){
// cout<<u<<" "<<v<<endl;
edge[++cnt].nxt=hd[u];
edge[cnt].to=v;
hd[u]=cnt;
return ;
}
void insert(char* str,int id){
len[id]=strlen(str+1);
int p=0;
for(int i=1;i<=len[id];i++){
int x=str[i]-'a';
if(!trie[p][x])trie[p][x]=++tot;
p=trie[p][x];
b[id].push_back(p);
}
endpos[id]=p;
en[p]=id;
return ;
}
void build(){
int hd=1,tl=0;
for(int i=0;i<26;i++)if(trie[0][i])q[++tl]=trie[0][i],add(0,trie[0][i]);
while(hd<=tl){
int p=q[hd++];
for(int i=0;i<26;i++){
int x=trie[p][i];
if(!x)trie[p][i]=trie[nxt[p]][i];
else{
nxt[x]=trie[nxt[p]][i];
add(nxt[x],x);
if(!en[x])en[x]=en[nxt[x]];
q[++tl]=x;
}
}
}
return ;
}
void dfs(int u){
st[u]=++num;
for(int i=hd[u];i;i=edge[i].nxt){
int v=edge[i].to;
dfs(v);
}
ed[u]=num;
return ;
}
void add1(int x,int w){
x++;for(;x<=num+1;x+=x&-x)c[x]+=w;
return ;
}
int ask(int x){
int res=0;x++;
for(;x;x-=x&-x)res+=c[x];
return res;
}
int main(){
n=read();
for(int i=1;i<=n;i++)scanf("%s",a+1),insert(a,i);
build();dfs(0);
for(int i=1;i<=n;i++){
int last=inf;
for(int j=b[i].size()-1;j>=0;j--){
int x=b[i][j];
if(j==b[i].size()-1)x=nxt[x];
if(en[x]){
if(j-len[en[x]]+1<last){
last=j-len[en[x]]+1,vis.push_back(en[x]);
viscnt[en[x]]++;
}
}
add1(st[b[i][j]],1);
}
for(int j=0;j<vis.size();j++){
int p=ask(ed[endpos[vis[j]]])-ask(st[endpos[vis[j]]]-1);
// cout<<vis[j]<<" "<<p<<" "<<viscnt[vis[j]]<<" ";
if(p==viscnt[vis[j]])ans++;
viscnt[vis[j]]=0;
}
// puts("");
vis.clear();
for(int j=0;j<b[i].size();j++)add1(st[b[i][j]],-1);
}
cout<<ans;
return 0;
}
非常规题
发现一个串能够延续本质相当于在 自动机上有一个没被打过标记的儿子可以走过去,而无限延伸需要自动机上有一个没被标记的环即可
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=30005;
const int maxm=1000005;
int trie[maxn][26],nxt[maxn],n,tot,q[maxn];
char c[maxn];
bool vis[maxn],vis1[maxn],vis2[maxn];
void insert(char* str){
int len=strlen(str+1),p=0;
for(int i=1;i<=len;i++){
int x=str[i]-'0';
if(!trie[p][x]){
trie[p][x]=++tot;
}
p=trie[p][x];
}
vis[p]=true;
return ;
}
void build(){
int hd=1,tl=0;
for(int i=0;i<2;i++){
if(trie[0][i]){
q[++tl]=trie[0][i];
}
}
while(hd<=tl){
int t=q[hd++];
for(int i=0;i<2;i++){
int x=trie[t][i];
if(!x)trie[t][i]=trie[nxt[t]][i];
else{
nxt[x]=trie[nxt[t]][i];
q[++tl]=x;
vis[x]|=vis[nxt[x]];
}
}
}
return ;
}
void dfs(int u){
if(vis1[u]){
cout<<"TAK";
exit(0);
}
if(vis[u]||vis2[u])return ;
vis1[u]=true;
vis2[u]=true;
for(int i=0;i<=1;i++){
int v=trie[u][i];
dfs(v);
}
vis1[u]=false;
return ;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
scanf("%s",c+1);
insert(c);
}
build();
dfs(0);
cout<<"NIE";
return 0;
}
CF1202E You Are Given Some Strings...
第一步就没有想到……
发现题目要求的是拼接 去匹配 ,那么可以拆分 来匹配
枚举 上的一个断点,断点前的一个后缀匹配一个 ,断点后的一个前缀匹配一个 ,那么两部分都转化为 自动机可以维护的经典问题
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int n,m,f1[maxn],f2[maxn];
long long ans;
char a[maxn],b[maxn];
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;
}
struct ACAM{
int trie[maxn][26],tot,q[maxn],nxt[maxn],cnt[maxn];
void insert(char* str){
int len=strlen(str+1),p=0;
for(int i=1;i<=len;i++){
int x=str[i]-'a';
if(!trie[p][x])trie[p][x]=++tot;
p=trie[p][x];
}
cnt[p]++;
return ;
}
void build(){
int hd=1,tl=0;
for(int i=0;i<26;i++){
if(trie[0][i])q[++tl]=trie[0][i];
}
while(hd<=tl){
int p=q[hd++];
for(int i=0;i<26;i++){
int x=trie[p][i];
if(!x)trie[p][i]=trie[nxt[p]][i];
else{
nxt[x]=trie[nxt[p]][i];
cnt[x]+=cnt[nxt[x]];
// cout<<x<<" "<<cnt[x]<<endl;
q[++tl]=x;
}
}
}
return ;
}
void ask(char* str,int* f){
for(int i=1,j=0;i<=n;i++){
int x=str[i]-'a';
j=trie[j][x];
f[i]=cnt[j];
}
return ;
}
}pre,suf;
int main(){
scanf("%s",a+1);
n=strlen(a+1);m=read();
for(int i=1;i<=m;i++){
scanf("%s",b+1);
pre.insert(b);
int len=strlen(b+1);
reverse(b+1,b+len+1);
suf.insert(b);
}
pre.build();suf.build();
pre.ask(a,f1);reverse(a+1,a+n+1);suf.ask(a,f2);
// for(int i=1;i<=n;i++)cout<<f1[i]<<" "<<f2[i]<<endl;
for(int i=1;i<=n;i++)ans+=1ll*f1[i]*f2[n-i];
cout<<ans;
return 0;
}
一个没见过的重构套路——二进制分组
每次加入一个字符串时都新建一棵大小为 的 自动机
然后用类似于 一样的方式开始合并相同大小的自动机,那么最后留下的一定最多只有 个,而每个字符串最多经过 次重构
再来考虑删除的问题,发现维护的信息是具有可减性的,那么对于需要删除的串也建立一遍 自动机然后减一减即可
但是实现起来会非常麻烦,最主要来源于平常的 自动机默认根是零,而数组恰好也是零,而这里根不再是零时,数组相应地也要进行初始化的改变
遇到一个玄学问题,将 复制到 时会 ,而如果采用初始化为空格之后采用 的方式来插入则不会有问题
:现在来看一点儿也不玄学了好吧
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e6+5;
int n,op,q[maxn];
char a[maxn];
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
return x*f;
}
struct{
int len[maxn],trie[maxn][26],tot,cnt[maxn],nxt[maxn],tp,num;
string b[maxn];
struct Node{
int l,r,siz,rt;
Node(){}
Node(int a,int b,int c,int d):l(a),r(b),siz(c),rt(d){}
}sta[maxn];
void insert(char* str,int rt){
int p=rt,len1=strlen(str+1);
for(int i=1;i<=len1;i++){
int x=str[i]-'a';
// cout<<"ppp "<<x<<endl;
if(trie[p][x]==rt){
trie[p][x]=++tot;
for(int j=0;j<26;j++)trie[tot][j]=rt;
}
p=trie[p][x];
}
cnt[p]++;
return ;
}
void build(int rt){
int hd=1,tl=0;
nxt[rt]=rt;
for(int i=0;i<26;i++)if(trie[rt][i]>rt)q[++tl]=trie[rt][i],nxt[trie[rt][i]]=rt;
while(hd<=tl){
int p=q[hd++];
for(int i=0;i<26;i++){
int x=trie[p][i];
if(x==rt)trie[p][i]=trie[nxt[p]][i];
else{
nxt[x]=trie[nxt[p]][i];
cnt[x]+=cnt[nxt[x]];
q[++tl]=x;
}
}
}
return ;
}
void merge(){
// cout<<"hhh"<<endl;
sta[tp-1].siz<<=1;sta[tp-1].r=sta[tp].r;tp--;
for(int i=sta[tp].rt;i<=tot;i++){
for(int j=0;j<26;j++)trie[i][j]=0;
nxt[i]=cnt[i]=0;
}
tot=sta[tp].rt;
for(int i=0;i<26;i++)trie[tot][i]=tot;
for(int i=sta[tp].l;i<=sta[tp].r;i++)insert(&b[i][0],sta[tp].rt);//,cout<<"do this "<<i<<endl;
build(sta[tp].rt);
return ;
}
void add(int i,char* a){
sta[++tp]=Node(i,i,1,++tot);
// cout<<tot<<" "<<sta[tp].rt<<endl;
for(int i=0;i<26;i++)trie[tot][i]=tot;
insert(a,tot);build(sta[tp].rt);
while(tp>1&&sta[tp].siz==sta[tp-1].siz)merge();
return ;
}
int doask(char* str,int j){
int res=0,len1=strlen(str+1);
for(int i=1;i<=len1;i++){
int x=str[i]-'a';
j=trie[j][x];res+=cnt[j];
// cout<<j<<" ";
}
return res;
}
int ask(char* a){
int res=0;
for(int i=1;i<=tp;i++)res+=doask(a,sta[i].rt);
return res;
}
}_add,_del;
int main(){
n=read();
for(int i=1;i<=n;i++){
op=read();
scanf("%s",a+1);
if(op==1){
_add.len[++_add.num]=strlen(a+1);
_add.b[_add.num]=" ";
for(int i=1;i<=_add.len[_add.num];i++)_add.b[_add.num]+=a[i];
_add.add(_add.num,a);
}
else if(op==2){
_del.len[++_del.num]=strlen(a+1);
_del.b[_del.num]=" ";
for(int i=1;i<=_del.len[_del.num];i++)_del.b[_del.num]+=a[i];
_del.add(_del.num,a);
}
else{
printf("%d\n",_add.ask(a)-_del.ask(a));
fflush(stdout);
}
}
return 0;
}
Summary
- 自动机能干得事无非就是多模匹配,直接往这方面想即可
- 套路主要就是 和树上问题, 经常容易漏掉
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效