AC自动机--summer-work之我连模板题都做不出

2023-03-18,,

这章对现在的我来说有点难,要是不写点东西,三天后怕是就一无所有了。

但写这个没有营养的blog的目的真的不是做题或提升,只是学习学习代码和理解一些概念。

现在对AC自动机的理解还十分浅薄,这里先贴上目前我看过的文章:

    深入理解Aho-Corasick自动机算法
    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[][], fail[], end[];
int root, L;
int newnode(){
for(int i=; i<; i++){
next[L][i] = -;
}
end[L++] = ;
return L-;
}
void init(){
L = ;
root = newnode();
}
void insert(char buf[]){
int len = strlen(buf);
int now = root;
for(int i=; i<len; i++){
if(next[now][buf[i]-'a'] == -){
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=; i<; i++){
if(next[root][i] == -){
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=; i<; i++){
if(next[now][i] == -){
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 = ;
for(int i=; i<len; i++){
now = next[now][buf[i]-'a'];
int temp = now;
while(temp != root){
res += end[temp];
end[temp] = ;
temp = fail[temp];
}
}
return res;
}
void debug(){
for(int i=; i<L; i++){
cout << "id = " << i << " " << "fail = " << fail[i] << " " << "end = " << end[i] << endl;
for(int j=; j<; j++){
cout << next[i][j] << " ";
}
cout << endl;
}
}
};
char buf[];
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=; i<n; i++){
scanf("%s", buf);
ac.insert(buf);
}
ac.build();
scanf("%s", buf);
cout << buf << endl;
printf("%d\n", ac.query(buf));
}
return ;
}

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[][], fail[], val[];
int res, sz;
inline void init(){ sz=; mst(ch[], ); mst(val, ); }
inline int idx(char c) { return c-'A'; }
inline void insert(char *s){
int u = , n = strlen(s);
for(int i=; i<n; i++){
int c = idx(s[i]);
if(!ch[u][c]){
mst(ch[sz], );
ch[u][c] = sz++;
}
u = ch[u][c];
}
val[u] = ;
}
inline void build(){
queue<int> Q;
int u; fail[] = ;
for(int i=; i<; i++){
u = ch[][i];
if(u){ fail[u]=; Q.push(u); }
}
while( !Q.empty() ){
int now = Q.front(); Q.pop();
for(int i=; i<; 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 = , res = ;
for(int i=; i<len; i++){
int c = idx(buf[i]);
u = ch[u][c];
int temp = u;
//有几个前缀出现在文本串中
while(temp && val[temp]!=){
res += val[temp];
val[temp] = ;
temp = fail[temp];
}
}
return res;
}
};
char tmp[], buf[];
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=; i<n; i++){
scanf("%s", buf);
ac.insert(buf);
}
ac.build();
scanf("%s", tmp);
int tlen = strlen(tmp), blen=;
for(int i=; i<tlen; i++){
if(tmp[i]=='['){
i++;
int cnt = ;
while(tmp[i] >= '' && tmp[i] <= ''){
cnt = cnt* + tmp[i++]-'';
}
for(int k=; k<cnt; k++){
buf[blen++] = tmp[i];
}
i++;
}else{
buf[blen++] = tmp[i];
}
}
tmp[blen] = '\0';
for(int i=; i<blen; i++){
tmp[i] = buf[blen-i-];
}
buf[blen] = '\0';
printf("%d\n", ac.query(buf, blen) + ac.query(tmp, blen));
}
return ;
}

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+;
const int sigma_size = ;
struct Trie{
int ch[maxnode][sigma_size], val[maxnode], f[maxnode], last[maxnode], sz;
int cnt[maxnode], dis[maxnode];
inline void init(){ sz=; mst(ch[], ); mst(val, ); mst(cnt, ); }
inline int idx(char x){ return tolower(x)-'a'; }
inline void insert(char *s){
int u = , n = strlen(s);
for(int i=; i<n; i++){
int c = idx(s[i]);
if(!ch[u][c]){
mst(ch[sz], );
ch[u][c] = sz++;
}
u = ch[u][c];
}
val[u] = ;
dis[u] = n;
}
inline void get_fail(){
queue<int> Q;
int u; f[] = ;
for(int c=; c<sigma_size; c++){
u = ch[][c];
if(u) { last[u] = f[u]=; Q.push(u); }
}
while(!Q.empty()){
int now = Q.front(); Q.pop();
for(int c=; 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 = ;
for(int i=; 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+];
--cnt[i+-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 = , len = strlen(s);
for(int i=; i<len; i++){
ans += ac.cnt[i];
if(ans < ) putchar('*');
else putchar(s[i]);
}
}
return ;
}

还有就是我有一个疑惑想了很久没有解决,也找了万老师解惑,虽然并没有得到很令我心服的答案,但依然很感谢万老师的指导。

训练指南245页有关last指针的分析,他说后缀链接last[j]是结点j沿着失配指针往回走。我的疑惑是:这个last指针和fail有什么区别,或是二者回跳方式有何不同。如果有哪位聚聚或老师对这个问题有过思考请不吝指教,或指导下学习方式等,本菜鸡定感激不尽。

AC自动机--summer-work之我连模板题都做不出的相关教程结束。

《AC自动机--summer-work之我连模板题都做不出.doc》

下载本文的Word格式文档,以方便收藏与打印。