浅析 AC 自动机

news/2025/10/2 8:29:09/文章来源:https://www.cnblogs.com/doooge/p/19123176

哈喽大家好,我是 doooge,今天来点大家想看的东西啊。

\[\Huge \sf 浅析~AC~自动机 \]

前置知识:Trie,不需要 KMP。

1. AC 自动机的构造与匹配

所谓 AC 自动机,是结合了 Trie 和 KMP 思想的自动机,简单来说就是一个 Trie 图,用于解决字符串多模式匹配的问题。

AC 自动机由两个部分构成:原本的 Trie 和若干个失配指针
\(fail\),下面给一道它的例题。

有一个文本串 \(T\)\(n\) 个模式串 \(S_i\),求有几个模式串是否在文本串中出现过。
\((n,|T|\le 10^6,\sum_{i=1}^{n}|S_i|\le 10^6)\),字符串中只包含小写字母。

如果用暴力匹配,复杂度是 \(O(n^2)\) 的,不够优秀。直接讲 AC 自动机的做法。

1.1 建树

构建 AC 自动机的第一步是将所有的模式串加入 Trie,跟普通的 Trie 一样。

例如,有 \(3\) 个字符串 ABCDABDBD,建完的树是这样的,记 \(x\) 的字符为 \(y\) 为子节点为 \(t_{x,y}\)

值得注意的是,Trie 上的每一个节点都是一个模式串的前缀(这也是字典树的性质)

但是,就算建好了这棵树,我们也不好匹配,因为我们不知道模式串是在文本串的哪个位置出现的,这个时候,就要用到我们的失配指针 \(fail\) 了。

1.2 失配指针

定义 \(i\) 属于的子串为从根节点 \(Root\) 所出发到节点 \(i\),途中经过的字符组成的字符串。

失配指针是用于辅助文本串转移的,记 \(fail_x\)\(x\)\(fail\) 指针。

我的理解是,\(fail_i\) 表示的是,\(i\) 所属于的子串的后缀是某个模式串的前缀,且前缀 / 后缀尽可能的长,这个前缀所对应的字符串在 Trie 中的节点为 \(j\)

很抽象,用图来解释吧:

因为节点 \(5\) 属于的子串为 ABD,节点 \(7\) 属于的子串为 BD 且是图中所有最长的后缀(当然也只有它一个),所以 \(fail_5=7\)

如果多了一个模式串 D,它们之间的 \(fail\) 指针是这样的:

那么,我们如何才能求出 \(fail\) 指针呢?我们假设已经知道 \(fa\)\(fail\) 指针,如何才能求出子节点 \(fail\) 的指针呢?

我们知道子节点想要后缀最大,肯定是继承父节点 \(fail\) 指针最优,比如说下图:

首先,我们先查找节点 \(5\) 的父节点的 \(fa_5=2\),因为 \(2\) 号节点的 \(fail\) 节点 \(6\) 有相邻为 D 的子节点,所以 \(fail_5=t_{6,D}=7\)

但是还有一个特殊情况,这里同样给个例子

首先,我们同样查找节点 \(3\) 的父节点的 \(fa_3=2\),因为 \(2\) 号节点的 \(fail\) 节点 \(6\) 并没有相邻边为 D 的子节点,所以我们想要答案最优,就得继续跳,我们跳到 \(fail_6\),也就是根节点,虽然这里仍然没有相邻为 D 的边,但是此时的根节点已经跳不动了,所以 \(fail_3=0\),也就是根节点。

但是,如果一直暴力跳 \(fail\),复杂度最坏是 \(O(n)\) 的,我们如何才能优化呢?我们可以建若干个虚拟节点,而这个节点指向它的 \(fail\)

如图,我们在 \(4\) 号节点下建一个用 C 边连接的虚拟节点 BC,指向的是 BC\(fail\),我们要建 ABC\(fail\) 时就能直接查询到 BC 这个节点,进而查到 C 这个 \(fail\),因为原本的 Trie 上没有 BC 这个节点,所以一定能保证后续我们通过虚拟节点查到的 \(fail\) 一定时最优的。

下面给了个更加复杂的例子,这里就不作过多解释了。

进一步,我们可以用 BFS 来求,使得求到 \(x\)\(fail\) 时深度比 \(x\) 小的能先求出来。

于是,构建 \(fail\) 指针的步骤是这样的:

  1. 将根节点的所有子节点入队,根节点的虚拟节点的 \(fail\) 都指向根。
  2. 每次取出队首的节点 \(x\),枚举每一个字符 \(i\),若 \(t_{x,i}\) 是空的,建出 \(x\) 的所有虚拟节点指向那个节点的 \(fail\),可以写成:

\[\large t_{x,i}=fail_{t_{x,i}}=t_{fail_x,i} \]

此时 \(t_{x,i}\)\(fail\) 指针一定不会是空的,因为深度小于 \(x\) 的节点的虚拟节点已经求完了。

  1. 否则,建出 \(t_{x.i}\)\(fail\) 指针并将其入队:

\[\large fail_{t_{x,i}}=t_{fail_x,i} \]

  1. 重复 \(2\)\(3\) 操作,直到队列为空停止。

下面用图解 \(fail\) 指针建造过程(模式串只有 ABC 三种字符构成):

首先,我们将根节点的子节点 \(1,5,7\) 入队(忘画 \(4\) 节点见谅):

先取出节点 \(1\),构建 \(1\) 的虚拟节点和子节点 \(2\)\(fail\),将 \(2\) 入队:

再取出节点 \(5\),建出虚拟节点和子节点 \(6\)\(fail\)(这里 \(6\)\(fail\) 画成虚边了见谅,后面改回来了),将 \(6\) 入队:

如法炮制节点 \(7,2,6,3\),然后就画成了这么一幅图:

1.3 AC 自动机的匹配

AC 自动机的匹配过程如下(文本串为 \(T\)):

  1. 初始 \(pos\) 节点为根(\(0\))。
  2. \(i\)\(1\)\(|T_i|\),重复 \(2,3\) 操作,每次寻找与 \(pos\) 节点相邻为 \(T_i\) 的边,\(pos \gets t_{pos,T_i}\)
  3. \(k\)\(pos\),每次暴力往上面跳 \(k=fail_k\),过程中打上标记 \(vis_k=1\),只要发现 \(vis_k\) 之前被标记过了就停止,途中记录经过了哪些节点在 Trie 上是某个模式串的结尾节点。
  4. 最后看有哪些模式串所在的节点被经过了就是答案。

匹配过程如下(还是之前的那一幅图):

时间复杂度:\(O(26n)\) 也就是 \(O(n)\),实际跑起来速度跟 \(O(n \log n)\) 差不多。

至此,你已经掌握 AC 自动机的大部分了!

2. 代码

模板题:P3808。

#include<bits/stdc++.h>
using namespace std;
struct ll{int flag,fail,son[30];//flag表示这个节点有几个模式串的尾节点 
}t[1000010];
int cnt=1,ans;//根节点从1开始,方便等下fail处理 
bool vis[1000010];
void build(string s){//就是普通的Trie插入 int pos=1;for(int i=0;i<s.size();i++){if(!t[pos].son[s[i]-'a']){t[pos].son[s[i]-'a']=++cnt;}pos=t[pos].son[s[i]-'a'];}t[pos].flag++;return;
}
void build_fail(){queue<int>q;for(int i=0;i<26;i++)t[0].son[i]=1;t[1].fail=0;q.push(1);//我习惯这么写,当然可以把根的子节点都丢进去 while(!q.empty()){int tmp=q.front();q.pop();for(int i=0;i<26;i++){if(!t[tmp].son[i]){//构建虚拟节点 t[tmp].son[i]=t[t[tmp].fail].son[i];continue;//这时就不用入队了 }t[t[tmp].son[i]].fail=t[t[tmp].fail].son[i];q.push(t[tmp].son[i]);//处理子节点 }}return;
}
void query(string s){int pos=1;for(int i=0;i<s.size();i++){pos=t[pos].son[s[i]-'a'];int k=pos;while(!vis[k]){//跳fail ans+=t[k].flag,vis[k]=true;k=t[k].fail; } }return;
}
int main(){int n;string s;cin>>n;for(int i=1;i<=n;i++){string x;cin>>x;build(x);}build_fail(); cin>>s;query(s);cout<<ans<<endl;return 0;
}

3. 拓扑建图优化

还是放一道例题(P5357):

有一个文本串 \(T\)\(n\) 个模式串 \(S_i\),求每个模式串在文本串中出现的次数。
\((n,|T|\le 10^6,\sum_{i=1}^{n}|S_i|\le 10^6)\),字符串中只包含小写字母。

求出每个模式串的个数可不好搞,因为一个模式串尾节点可能被访问过多次,只用 \(vis\) 数组标记肯定是不正确的,但是暴力跳 \(fail\) 的时间实在太长。

我们可以回想一下刚刚的查询是怎么找答案的,我们能否优化这个跳 \(fail\) 的过程呢?答案是肯定的,比如说下面这幅图(蓝色表示经过的节点):

我们发现 \(fail\) 指针只会指向深度比自己低的节点,也就是说一直跳 \(fail\) 不会遇到一个环,这不就是一张有向无环图吗?

我们把 \(fail\) 指针看成边,然后就可以进行拓扑来求到答案,因为我们知道如果能够匹配到 \(x\),也一定能够匹配到 \(fail_x\)

完整代码如下:

#include<bits/stdc++.h>
using namespace std;
const int Root=1;//Root=1
struct ll{//trieint fail,flag,ans,son[35];//flag表示有几个模式串的结尾在这个节点 
}t[200010];
int ans[200010],vis[200010],in[200010],mp[200010];
int cnt=1;//节点数量 
void build(string s,int q){//trie内添加s int pos=Root;for(int i=0;i<s.size();i++){if(t[pos].son[s[i]-'a']==0){t[pos].son[s[i]-'a']=++cnt;}pos=t[pos].son[s[i]-'a'];
//		cout<<pos<<endl;}if(!t[pos].flag)t[pos].flag=q;mp[q]=t[pos].flag;return;
}
void build_fail(){//0的子节点指向Root,Root的Fail指针指向0 fill(t[0].son,t[0].son+26,Root);queue<int>q;t[Root].fail=0;q.push(Root);while(!q.empty()){int tmp=q.front();q.pop();
//		cout<<tmp<<endl;for(int i=0;i<26;i++){//父亲转移儿子int v=t[tmp].son[i];if(v==0){//没有这个儿子 t[tmp].son[i]=t[t[tmp].fail].son[i];//t[tmp].son[i]更新为t[t[tmp].son[i]].failcontinue;}t[v].fail=t[t[tmp].fail].son[i];//t[tmp].fail一定有i这个儿子 in[t[v].fail]++;//x->fail[x]q.push(v);}}return;
}
void query(string s){//寻找 int pos=Root;for(int i=0;i<s.size();i++){pos=t[pos].son[s[i]-'a'];t[pos].ans++;}return;
}
void topo(){queue<int>q;for(int i=1;i<=cnt;i++){if(in[i]==0)q.push(i);}while(!q.empty()){int tmp=q.front();q.pop();vis[t[tmp].flag]=t[tmp].ans;int x=t[tmp].fail;in[x]--,t[x].ans+=t[tmp].ans;if(in[x]==0)q.push(x);}return;
}
int main(){ios::sync_with_stdio(0);int n;string s;cin>>n;for(int i=1;i<=n;i++){string x;cin>>x;build(x,i);}build_fail();
//	cout<<"input end.\n";cin>>s;memset(ans,0,sizeof(ans));query(s);topo();for(int i=1;i<=n;i++){cout<<vis[mp[i]]<<endl;}return 0;
}

4. AC 自动机上 DP

先给个例题(P3041):

\(n\) 个模式串 \(s_i\),构造一个长度为 \(k\) 的字符串,使得所有模式串在这个字符串中出现次数之和最大,询问这个最大的值是多少。
\(n\le20\)\(|s_i|\le15\)\(k\le10^3\)),字符串只有 ABC 这三个字符组成。

直接来想 DP 如何做这道题,我们先想想状态如何构造。

不难想到,我们可以设字符串的长度 \(i\),字符为 \(j\) 结尾最大的答案为 \(dp_{i,j}\)

但是怎么转移呢?因为我们只知道最后字符的长度,假设我们枚举的状态 \(dp_{i,B}\)...B,此时来了一个 ABA 的模式串,\(dp_{i+1,A}\) 肯定会加上这段贡献,但是我们如何才能区分 \(i-1\) 填的是什么呢?所以这个状态设计的不对。

我们来考虑在 DP 上套 AC 自动机,首先,长度这个状态肯定是要保留的,我们可以新增加一个维度表示此时在 Trie 上的哪个节点,也就是说,\(dp_{i,j}\) 表示的是长度为 \(i\)\(1\)\(i\) 所构成的字符串上以节点 \(j\) 结尾。

那么转移也就很简单了:

\[dp_{i+1,t_{j,k}}=dp_{i,j}+cnt_{t_{j,k}}(k\in \{A,B,C\}) \]

\(cnt_x\) 表示的是多少个模式串是节点 \(x\) 属于的子串的后缀,这个可以暴力跳 \(fail\) 或者在处理 \(fail\) 指针是就将其处理好。

答案在 \(dp_{n,i}\) 中取 \(\max\) 就行了!

完整代码:

#include<bits/stdc++.h>
using namespace std;
struct ll{int fail,flag,son[20];
}t[310];
int dp[1010][310],cnt=1;
void build(string s){int pos=1;for(int i=0;i<s.size();i++){if(t[pos].son[s[i]-'A']==0){t[pos].son[s[i]-'A']=++cnt;}pos=t[pos].son[s[i]-'A'];}t[pos].flag++;return;
}
void build_fail(){for(int i=0;i<3;i++){t[0].son[i]=1;}t[1].fail=0;queue<int>q;q.push(1);while(!q.empty()){int tmp=q.front();q.pop();for(int i=0;i<3;i++){int x=t[tmp].son[i];if(x==0){t[tmp].son[i]=t[t[tmp].fail].son[i];continue;}t[x].fail=t[t[tmp].fail].son[i];q.push(x);}t[tmp].flag+=t[t[tmp].fail].flag;}return;
}
int main(){int n,m,ans=0;cin>>n>>m;for(int i=1;i<=n;i++){string s;cin>>s;build(s);}build_fail();for(int i=0;i<=m;i++){for(int j=2;j<=cnt;j++){dp[i][j]=-1e9;}}for(int i=1;i<=m;i++){for(int j=1;j<=cnt+1;j++){for(int k=0;k<3;k++){dp[i][t[j].son[k]]=max(dp[i][t[j].son[k]],dp[i-1][j]+t[t[j].son[k]].flag);}}}for(int i=0;i<=cnt;i++){ans=max(ans,dp[m][i]);}cout<<ans<<endl;return 0;
}

总结:AC 自动机上的 DP 一般都会有长度和节点位置这两项状态。

5. 例题

5.1 P2444 [POI 2000] 病毒

\(n\) 个由 \({0,1}\) 组成的模式串,问能否构造一个长度无限只包含 \({0,1}\) 的字符串不包含任何的模式串,如果能,输出 TAK,否则输出 NIE
\(1\le n\le 2\times 10^3\),所有模式串的长度不超过 \(3\times 10^4\)

首先,一个满足条件无限长的字符串,一定可以按一种循环结尾。

比如说这个例子:

我们想要一个满足条件的字符串,在匹配时就不能经过模式串尾节点或直接 / 间接通过 \(fail\) 能到达的模式串尾节点的节点,那我们应该怎样走呢?

不难看出,我们一定要绕着一条环走,举个例子,这个样例中 100100... 这样的满足条件字符串在匹配过程中就绕着 \(Root,1,4\) 再到 \(4\) 的虚拟节点回到 \(Root\) 这样的环走111...000... 也可以。

所以,我们只要先求出不能走到哪些点,在判断剩下的点是否包含了环就行了。

完整代码:

#include<bits/stdc++.h>
using namespace std;
int n,cnt=1;
bool vis[30010],f[30010];
struct Trie{int fail,son[5];bool flag;
}t[30010];
void build(string s){int pos=1;for(int i=0;i<s.size();i++){if(!t[pos].son[s[i]-'0']){t[pos].son[s[i]-'0']=++cnt;}pos=t[pos].son[s[i]-'0'];}t[pos].flag=true;
}
void build_fail(){for(int i:{0,1})t[0].son[i]=1;t[1].fail=0;queue<int>q;q.push(1);while(!q.empty()){int tmp=q.front();q.pop();for(int i:{0,1}){if(!t[tmp].son[i]){t[tmp].son[i]=t[t[tmp].fail].son[i];continue;}t[t[tmp].son[i]].fail=t[t[tmp].fail].son[i];t[t[tmp].son[i]].flag|=t[t[t[tmp].fail].son[i]].flag;q.push(t[tmp].son[i]);}}return;
}
void dfs(int x){if(vis[x]){cout<<"TAK\n";exit(0);}vis[x]=1;for(int i:{t[x].son[0],t[x].son[1]}){if(f[i]||t[i].flag)continue;dfs(i);f[i]=1;}vis[x]=0;return;
}
int main(){cin>>n;for(int i=1;i<=n;i++){string s;cin>>s;build(s);}build_fail();dfs(1);cout<<"NIE\n";return 0;
}

5.2 P2414 [NOI2011] 阿狸的打字机

阿狸喜欢收藏各种稀奇古怪的东西,最近他淘到一台老式的打字机。打字机上只有 \(28\) 个按键,分别印有 \(26\) 个小写英文字母和 BP 两个字母。经阿狸研究发现,这个打字机是这样工作的:

  • 输入小写字母,打字机的一个凹槽中会加入这个字母(这个字母加在凹槽的最后)。
  • 按一下印有 B 的按键,打字机凹槽中最后一个字母会消失。
  • 按一下印有 P 的按键,打字机会在纸上打印出凹槽中现有的所有字母并换行,但凹槽中的字母不会消失。
    我们把纸上打印出来的字符串从 \(1\) 开始顺序编号,一直到 \(n\)。有 \(m\) 次询问,每次询问第 \(x\) 次被打印出来的字符串在第 \(y\) 次被打印出来的字符串中出现过多少次。

这道题考察了对 \(fail\) 指针的理解。

我们知道若 \(fail_x=y\),那么 \(x\) 属于的子串一定包含 \(y\) 所属的子串。

所以,这道题就变成了,属于模式串 \(y\) 的节点中,有多少个节点的 \(fail\) 直接或间接的指向 \(x\)

进一步我们可以用 \(fail\) 指针构成一棵 \(fail\) 树,此时 \(fail\) 树的节点就是原 Trie 的节点,但只有 \(fail_x\) 连向 \(x\) 的边:

于是,这道题就变成了:在 \(fail\) 树种以模式串 \(x\) 结尾所在的节点的子树中,有多少个节点在 Trie 中是属于模式串 \(y\) 的。

这样看起来似乎还是不太好搞,但是我们知道一条性质:子树内的 \(dfn\) 是连续的,我们似乎能通过这个 \(dfn\) 来维护信息,而这个似乎可以用树状数组来维护。

我们先考虑将将询问存储 \(x,y\) 存到模式串 \(x\) 尾节点里,然后呢,我们可以 DFS 整个 \(fail\) 树,找到每个节点在 \(fail\) 树中所对应的 \(dfn\) 和每个子树的大小。

然后,我们可以再根据输入的信息来模拟:每次输入小写字母时就更新树状数组在 \(dfn_x\) 的位置上打上标记,遇到 B 时就先将打上的标记撤销,令 \(x\gets fa_x\),这个 \(fa\) 数组提前就可以处理好,最后,遇到询问操作 P 的时候就直接枚举这个节点存过的询问就行了。

时间复杂度:\(O(n\log n)\),当然因为常数问题速度跟 \(O(n\log n^2)\) 差不多了。

完整代码:

#include<bits/stdc++.h>
using namespace std;
struct node{int flag,fa,fail,son[30];
}t[100010];
struct Query{int x,id;
};
int tr[100010],dfn[100010],siz[100010],dep[100010],ans[100010],endpos[100010],cnt=1,pos=1,cntq,tot,q;
vector<int>v[100010];
vector<Query>Q[100010];
int lowbit(int x){return x&-x;
}
void build(int x,int y){for(int i=x;i<=cnt+1;i+=lowbit(i)){tr[i]+=y;}return;
}
int query(int x){int ans=0;for(int i=x;i;i-=lowbit(i)){ans+=tr[i];}return ans;
}
void insert(char ch){if(!t[pos].son[ch-'a'])t[pos].son[ch-'a']=++cnt;t[t[pos].son[ch-'a']].fa=pos;pos=t[pos].son[ch-'a'];return;
}
void build_fail(){for(int i=0;i<26;i++){t[0].son[i]=1;}t[1].fail=0;queue<int>q;q.push(1);while(!q.empty()){int tmp=q.front();q.pop();for(int i=0;i<26;i++){if(!t[tmp].son[i]){t[tmp].son[i]=t[t[tmp].fail].son[i];continue;}t[t[tmp].son[i]].fail=t[t[tmp].fail].son[i];q.push(t[tmp].son[i]);}}return;
}
void build_graph(){for(int i=1;i<=cnt;i++){v[i].push_back(t[i].fail);v[t[i].fail].push_back(i);}return;
} 
void dfs(int x,int fa){dfn[x]=++tot,siz[x]=1;for(int i:v[x]){if(i==fa)continue;dfs(i,x);siz[x]+=siz[i];}return;
}
int main(){string opt;cin>>opt>>q;for(char ch:opt){if(ch=='B'){pos=t[pos].fa;}else if(ch=='P'){t[pos].flag=++cntq;endpos[cntq]=pos;}else{insert(ch);}} build_fail();build_graph();dfs(0,-1);for(int i=1;i<=q;i++){int x,y;cin>>x>>y;Q[endpos[y]].push_back({endpos[x],i});}pos=1;for(char ch:opt){if(ch=='B'){build(dfn[pos],-1);pos=t[pos].fa;}else if(ch=='P'){for(Query i:Q[pos]){ans[i.id]=query(dfn[i.x]+siz[i.x]-1)-query(dfn[i.x]-1);}}else{pos=t[pos].son[ch-'a'];build(dfn[pos],1);}}for(int i=1;i<=q;i++){cout<<ans[i]<<endl;}return 0;
} 

5.3 P2292 [HNOI2004] L 语言

提醒:作者这个做法的复杂度时错的,能 AC 纯粹就是靠玄学优化卡过去的。

给定 \(n\) 个模式串 \(s_i\),定义一个字符串能够被理解为这个字符串可以拆成若干个模式串拼接而成,有 \(m\) 次询问,第 \(i\) 次询问 \(T_i\) 最长能够被理解的前缀子串的长度。
\(n\le 20,m\le 50,1\le |s|\le 20,\sum_{i=1}^{m}|T_i|\le 10^6\)\(s\)\(t\) 中均只含小写英文字母。

这题的正解时状压但是我没用也过了

我们设朴素的 \(dp_i\) 为询问字符串中以第 \(i\) 位结尾的子串能否被读懂。

转移的时候只要枚举每个模式串,暴力跳 \(fail\) 查询就行了,时间复杂度 \(O(n\sum|T_i|)\),凭借着庞大的常数成功 TLE。

于是我们可以做几个小优化:

  • 在跳 \(fail\) 时,如果发现 \(dp_i\) 已经为真就直接跳出循环。
  • 如果 \(dp_{i-mx}\)\(dp_i\) 都不为真,那么直接就能跳出循环了(\(mx\) 为最长的模式串的长度即 \(\max_{i=1}^{n}|s_i|\))。

然后这道题就能过了,完整代码如下:

#include<bits/stdc++.h>
using namespace std;
bool dp[2000010];
struct ll{int son[30],fail,flag;
}t[2010];
int cnt=1;
void build(string s){int pos=1;for(int i=0;i<s.size();i++){if(!t[pos].son[s[i]-'a']){t[pos].son[s[i]-'a']=++cnt;}pos=t[pos].son[s[i]-'a'];}t[pos].flag=s.size();return;
}
void build_fail(){for(int i=0;i<26;i++)t[0].son[i]=1;t[1].fail=0;queue<int>q;q.push(1);while(!q.empty()){int tmp=q.front();q.pop();for(int i=0;i<26;i++){if(!t[tmp].son[i]){t[tmp].son[i]=t[t[tmp].fail].son[i];continue;}t[t[tmp].son[i]].fail=t[t[tmp].fail].son[i];q.push(t[tmp].son[i]);}}return;
}
int main(){ios::sync_with_stdio(0);cin.tie(0),cout.tie(0);int n,m,mx=0;cin>>n>>m;for(int i=1;i<=n;i++){string s;cin>>s;build(s);mx=max(mx,int(s.size()));}build_fail();while(m--){string s;cin>>s;s=' '+s;fill(dp,dp+s.size(),false);int pos=1,last=0,ans=0;bool flag=false;dp[0]=true;for(int i=1;i<s.size();i++){pos=t[pos].son[s[i]-'a'];int k=pos;while(k>1){dp[i]|=dp[i-t[k].flag];if(dp[i])break;k=t[k].fail;}if(dp[i])last=i;else{if(i-last>mx){ans=last;flag=true;break;}}}if(flag)cout<<ans<<'\n';else{for(int i=1;i<s.size();i++){if(dp[i])ans=i;}cout<<ans<<'\n';}}return 0;
}

6. 作业

  1. P3796 AC 自动机(简单版 II)。
  2. P3121 [USACO15FEB] Censoring G
  3. P4052 [JSOI2007] 文本生成器

7. 闲话

说实话,这篇文章的图画了我很长时间尤其是画完之后发现是错的真的崩溃了

蒟蒻不才,膜拜大佬,如果文章有任何错字等问题,请在评论区提醒我。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/924701.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

实用指南:谷歌官方 Chrome DevTools MCP 正式发布

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

2025百度官网认证作用代理商推荐,北京益百科技通过官网认证,助力企业优化搜索排名,提升用户体验,降低营销成本

在当今数字化时代,互联网已成为企业宣传推广的主阵地。北京益百科技有限公司作为一家致力于为企业提供互联网解决方案的专业公司,自2014年与百度携手合作,成为北京地区百度信誉“首批独家”授权服务商以来,凭借其丰…

实用指南:Linux(操作系统)文件系统--对打开文件的管理

实用指南:Linux(操作系统)文件系统--对打开文件的管理pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas…

南昌建设局网站查询塔吊证怎么查网络优化工程师的工作内容

下载安装 下载地址: https://download.csdn.net/download/yijianxiangde100/88496463 安装apk 即可。 证书配置:

dede鲜花网站模板下载境外公司注册代理机构

前言 第11章对应的内容选择题和案例分析都会进行考查&#xff0c;这一章节属于10大管理的内容&#xff0c;学习要以教材为准。本章上午题分值预计在15分。 目录 11.13 制定预算 11.13.1 主要输入 11.13.2 主要输出 11.14 规划质量管理 11.14.1 主要输入 11.14.2 主要工…

上海营销型网站建设公司电商erp软件

语法上的小trick 构造函数 虽然不写构造函数也是可以的&#xff0c;但是可能会开翻车&#xff0c;所以还是写上吧。&#xff1a; 提供三种写法&#xff1a; ​ 使用的时候只用&#xff1a; 注意&#xff0c;这里的A[i]gg(3,3,3)的“gg”不能打括号&#xff0c;否则就是强制转换…

VMware ESXi 9.0.1.0 发布 - 领先的裸机 Hypervisor

VMware ESXi 9.0.1.0 发布 - 领先的裸机 HypervisorVMware ESXi 9.0.1.0 发布 - 领先的裸机 Hypervisor Standard (标准版)、Dell (戴尔)、HPE (慧与)、Lenovo (联想)、IEIT SYSTEMS (浪潮信息)、H3C (新华三)、Cisco…

VMware vSphere 9.0.1.0 发布 - 企业级工作负载平台

VMware vSphere 9.0.1.0 发布 - 企业级工作负载平台VMware vSphere 9.0.1.0 发布 - 企业级工作负载平台 ESXi 9.0 & vCenter Server 9.0 | vSphere 9.0 请访问原文链接:https://sysin.org/blog/vmware-vsphere-9/…

《索引实战:结构与场景解析》 - 详解

《索引实战:结构与场景解析》 - 详解pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Mona…

阿里云无影发布首个Agentic Computer形态的个人计算产品 - 详解

阿里云无影发布首个Agentic Computer形态的个人计算产品 - 详解pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Co…

响应式网站开发的wordpress 编辑器 空白

事实上&#xff0c;并不是我故意想成为一个困难的候选人。毕竟&#xff0c;在我加入这份工作后&#xff0c;我是一名同事&#xff0c;但面试官的角色是&#xff0c;如果高级面试官一般都是一样的话。 如果你在这里写&#xff0c;我担心一些想面试的朋友会害怕。如果他们有很强…

完整教程:iOS App 上架流程详解,苹果应用发布步骤、App Store 审核规则、ipa 文件上传与测试分发实战经验

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

Hadoop完全分布式配置 - 实践

Hadoop完全分布式配置 - 实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco"…

VMware Cloud Foundation Automation 9.0.1.0 发布 - 私有云自动化平台

VMware Cloud Foundation Automation 9.0.1.0 发布 - 私有云自动化平台VMware Cloud Foundation Automation 9.0.1.0 发布 - 私有云自动化平台 VMware Cloud Infrastructure - VCF Automation 请访问原文链接:https:/…

VMware Cloud Foundation Operations 9.0.1.0 发布 - 私有云运维管理

VMware Cloud Foundation Operations 9.0.1.0 发布 - 私有云运维管理VMware Cloud Foundation Operations 9.0.1.0 发布 - 私有云运维管理 VMware Cloud Infrastructure - VCF Operations 请访问原文链接:https://sys…

VMware Cloud Foundation Operations for Networks 9.0.1.0 发布 - 云网络监控与分析

VMware Cloud Foundation Operations for Networks 9.0.1.0 发布 - 云网络监控与分析VMware Cloud Foundation Operations for Networks 9.0.1.0 发布 - 云网络监控与分析 VMware Cloud Infrastructure - VCF Operatio…

网站模板素材住房与城乡建设网上办事大厅

随着公司的发展和市场竞争的影响&#xff0c;越来越多的创业者希望注册一家好名称的公司&#xff0c;以提高企业知名度和竞争力。但是&#xff0c;注册中字头无地域公司需要满足一定的条件和流程。本文将对中字头无地域公司注册条件及流程进行详细的介绍。可以致电咨询我或者来…

2025护栏板厂家TOP企业品牌推荐排行榜,波形护栏板、乡村、公路、道路、镀锌、喷塑、城乡、路侧、两波、三波护栏板推荐这十家公司!

在交通基础设施建设持续推进的当下,护栏板作为保障道路安全的关键设施,其质量与性能直接关系到行车安全与道路使用寿命。然而当前护栏板行业却面临诸多问题,部分生产厂家为压缩成本,在原材料选用上偷工减料,导致产…

网站建设域名注册免费百度seo怎么关闭

最近遇到一些事情&#xff0c;觉得挺憋屈的&#xff0c;可是再憋屈总得往前走吧&#xff01;打工人&#xff0c;不好办啊&#xff01;事情是这样的&#xff0c;笔者在芯片原厂负责SDK和行业解决方案输出的&#xff0c;可以理解成整体SDK turnkey方案。但是有些客户多少还要改一…

在AI技术唾手可得的时代,挖掘新需求成为核心竞争力——某知名AI开发框架需求洞察

本文深入分析了一个开源AI开发框架的核心功能与用户需求。该框架提供代码优先的开发方式、丰富的工具生态系统和多代理系统架构,支持从本地到云端的灵活部署。通过分析用户反馈,发现了对简化CLI工具、增强可视化界面…