AC自动机--summer-work之我连模板题都做不出
现在对AC自动机的理解还十分浅薄:
AC自动机相比Trie多了失配边,结点到结点间的状态转移,结点到根的状态转移。
这里fail的定义是:使当前字符失配时跳转到另一段从root开始每一个字符都与当前已匹配字符段某一个后缀完全相同且长度最大的位置继续匹配。
A - Keywords Search
题意:给多个模式串,求文本串中有多少模式串是子串。
这里直接贴上kuangbin大佬的模板。
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
struct Trie{
int next[500010][26], fail[500010], end[500010];
int root, L;
int newnode(){
for(int i=0; i<26; i++){
next[L][i] = -1;
}
end[L++] = 0;
return L-1;
}
void init(){
L = 0;
root = newnode();
}
void insert(char buf[]){
int len = strlen(buf);
int now = root;
for(int i=0; i<len; i++){
if(next[now][buf[i]-'a'] == -1){
next[now][buf[i]-'a'] = newnode();
}
now = next[now][buf[i]-'a'];
}
end[now]++;
}
void build(){
queue<int> Q;
fail[root] = root;
for(int i=0; i<26; i++){
if(next[root][i] == -1){
next[root][i] = root;
}else{
fail[next[root][i]] = root;
Q.push(next[root][i]);
}
}
while( !Q.empty() ){
int now = Q.front();
Q.pop();
for(int i=0; i<26; i++){
if(next[now][i] == -1){
next[now][i] = next[fail[now]][i];
}else{
fail[next[now][i]] = next[fail[now]][i];
Q.push(next[now][i]);
}
}
}
}
int query(char buf[]){
int len = strlen(buf);
int now = root;
int res = 0;
for(int i=0; i<len; i++){
now = next[now][buf[i]-'a'];
int temp = now;
while(temp != root){
res += end[temp];
end[temp] = 0;
temp = fail[temp];
}
}
return res;
}
void debug(){
for(int i=0; i<L; i++){
cout << "id = " << i << " " << "fail = " << fail[i] << " " << "end = " << end[i] << endl;
for(int j=0; j<26; j++){
cout << next[i][j] << " ";
}
cout << endl;
}
}
};
char buf[1000010];
Trie ac;
int main(){
freopen("in.txt", "r", stdin);
int T;
scanf("%d", &T);
while(T--){
int n;
scanf("%d", &n);
ac.init();
for(int i=0; i<n; i++){
scanf("%s", buf);
ac.insert(buf);
}
ac.build();
scanf("%s", buf);
cout << buf << endl;
printf("%d\n", ac.query(buf));
}
return 0;
}
C - L语言
D - Computer Virus on Planet Pandora
题意:给一个文本串,求其正反两个串中出现了几个模式串。
这里AC自动机中query()操作的作用:查询Trie中有几个前缀出现在文本串中。
方法是遍历Trie树找val=1的点即字符串最后一个字符,然后计数,同时要通过fail找回边,这里就是kmp的意味了。
所以才有人说AC自动机 = Trie+Kmp。
#include <cstdio>
#include <algorithm>
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
#define mst(s, t) memset(s, 0, sizeof(s));
struct Trie{
int ch[250110][26], fail[250010], val[250110];
int res, sz;
inline void init(){ sz=1; mst(ch[0], 0); mst(val, 0); }
inline int idx(char c) { return c-'A'; }
inline void insert(char *s){
int u = 0, n = strlen(s);
for(int i=0; i<n; i++){
int c = idx(s[i]);
if(!ch[u][c]){
mst(ch[sz], 0);
ch[u][c] = sz++;
}
u = ch[u][c];
}
val[u] = 1;
}
inline void build(){
queue<int> Q;
int u; fail[0] = 0;
for(int i=0; i<26; i++){
u = ch[0][i];
if(u){ fail[u]=0; Q.push(u); }
}
while( !Q.empty() ){
int now = Q.front(); Q.pop();
for(int i=0; i<26; i++){
u = ch[now][i];
if(!u){ ch[now][i] = ch[fail[now]][i]; continue; }
Q.push(u);
int v = fail[now];
while(v && !ch[v][i]) v=fail[v];
fail[u] = ch[v][i];
}
}
}
inline int query(char *buf, int len){
int u = 0, res = 0;
for(int i=0; i<len; i++){
int c = idx(buf[i]);
u = ch[u][c];
int temp = u;
//有几个前缀出现在文本串中
while(temp && val[temp]!=0){
res += val[temp];
val[temp] = 0;
temp = fail[temp];
}
}
return res;
}
};
char tmp[5100010], buf[5100010];
Trie ac;
int main(){
// freopen("in.txt", "r", stdin);
int T;
scanf("%d", &T);
while(T--){
ac.init();
int n;
scanf("%d", &n);
for(int i=0; i<n; i++){
scanf("%s", buf);
ac.insert(buf);
}
ac.build();
scanf("%s", tmp);
int tlen = strlen(tmp), blen=0;
for(int i=0; i<tlen; i++){
if(tmp[i]=='['){
i++;
int cnt = 0;
while(tmp[i] >= '0' && tmp[i] <= '9'){
cnt = cnt*10 + tmp[i++]-'0';
}
for(int k=0; k<cnt; k++){
buf[blen++] = tmp[i];
}
i++;
}else{
buf[blen++] = tmp[i];
}
}
tmp[blen] = '\0';
for(int i=0; i<blen; i++){
tmp[i] = buf[blen-i-1];
}
buf[blen] = '\0';
printf("%d\n", ac.query(buf, blen) + ac.query(tmp, blen));
}
return 0;
}
E - Family View
题意:找出文本串中所有模式串。
AC自动机的变化在insert()和find(),比较难想的应该是如何结合题意利用find()。
上题是计数模式串出现在文本串中的个数,而这道题则是每个模式串出现在文本串中的位置。
那么重点就是如何标记这些位置。
这题我用的是训练指南上的代码。
#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <cctype>
using namespace std;
#define mst(s, t) memset(s, t, sizeof(s))
const int maxnode = 1e6+10;
const int sigma_size = 26;
struct Trie{
int ch[maxnode][sigma_size], val[maxnode], f[maxnode], last[maxnode], sz;
int cnt[maxnode], dis[maxnode];
inline void init(){ sz=1; mst(ch[0], 0); mst(val, 0); mst(cnt, 0); }
inline int idx(char x){ return tolower(x)-'a'; }
inline void insert(char *s){
int u = 0, n = strlen(s);
for(int i=0; i<n; i++){
int c = idx(s[i]);
if(!ch[u][c]){
mst(ch[sz], 0);
ch[u][c] = sz++;
}
u = ch[u][c];
}
val[u] = 1;
dis[u] = n;
}
inline void get_fail(){
queue<int> Q;
int u; f[0] = 0;
for(int c=0; c<sigma_size; c++){
u = ch[0][c];
if(u) { last[u] = f[u]=0; Q.push(u); }
}
while(!Q.empty()){
int now = Q.front(); Q.pop();
for(int c=0; c<sigma_size; c++){
u = ch[now][c];
if(!u) { ch[now][c]=ch[f[now]][c]; continue; }
Q.push(u);
int v = f[now];
while(v && !ch[v][c]) v = f[v];
f[u] = ch[v][c];
last[u] = val[f[u]] ? f[u] : last[f[u]];
//{(he), (she)}5 --> {(he)}2
//last[5] = val[2]? 2 : last[f[2]]
//last[5] = 1 ? 2 : 0
//last[i]:表示沿着失配指针往回走时遇到的下一个单词结点编号
}
}
}
inline void find(char *s){
int n = strlen(s), u = 0;
for(int i=0; i<n; i++){
if(!isalpha(s[i]))continue; //这里要写,空格等乱七八糟的东西太多
int c = idx(s[i]);
u = ch[u][c];
if(val[u]) prin(i, u); //在字符串中找到前缀树中的内容后对字符串中的信息进行标记
else if(last[u]) prin(i, last[u]);
}
}
inline void prin(int i, int j){
if(j){
++cnt[i+1];
--cnt[i+1-dis[j]]; //[i+1-len, i+1)所有字符要forbid,故标记首尾
//cout << "i = " << i<< " last["<<j<<"] = "<<last[j]<< endl;
/*
* 这里last数组和fail数组回跳的方式有什么区别 和 它是如何提高效率的,
* 若有前辈知道,望直接指教,感激不尽。
*/
prin(i, last[j]);
}
}
/*
inline void show(){
for(int i=0; i<sz; i++){
cout<<"last["<<i<<"] = "<<last[i]<<" f["<<i<<"] = "<<f[i]<<endl;
}
}
*/
};
Trie ac;
char s[maxnode];
int main(){
freopen("in.txt", "r", stdin);
int t;
scanf("%d", &t);
while(t--){
ac.init();
int n;
scanf("%d", &n);
while(n--){
scanf("%s", s);
ac.insert(s);
}
ac.get_fail();
getchar();
fgets(s, maxnode, stdin);
ac.find(s);
int ans = 0, len = strlen(s);
for(int i=0; i<len; i++){
ans += ac.cnt[i];
if(ans < 0) putchar('*');
else putchar(s[i]);
}
}
return 0;
}
训练指南245页有关last指针的分析,他说后缀链接last[j]是结点j沿着失配指针往回走。我的疑惑是:这个last指针和fail有什么区别,或是二者回跳方式有何不同。如果有哪位老师对这个问题有过思考请不吝指教,或指导下学习方式等,本菜鸡定感激不尽。
作者:seaupnice
出处:https://www.cnblogs.com/seaupnice/p/9503188.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律