AC自动机从入门到last优化

AC自动机是KMP算法和Trie(字典树)的巧妙结合这篇文章主要讲针对几个例题给出解答模版(主要是知识点自己讲不清楚)。

至于针对的知识点,给上几个我认为说的比较好的传送门,读者可以自行选择阅读。(我就是看这几个大佬文章学的)

KMP :             https://blog.csdn.net/v_JULY_v/article/details/7041827?spm=1001.2014.3001.5502

Trie(字典树) :        https://blog.csdn.net/m0_46202073/article/details/107253959?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522164812432816780264091418%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=164812432816780264091418&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-2-107253959.142^v3^pc_search_result_control_group,143^v4^control&utm_term=%E5%AD%97%E5%85%B8%E6%A0%91&spm=1018.2226.3001.4187

AC自动机原版 :

https://www.cnblogs.com/cmmdc/p/7337611.html

https://www.cnblogs.com/sclbgw7/p/9260756.html

AC自动机的last优化 :       

https://www.cnblogs.com/sclbgw7/p/9875671.html

感谢以上大佬给出的解答%%%%。

下面我们针对几道模板例题进行解答。

P3808 【模板】AC 自动机(简单版)

https://www.luogu.com.cn/problem/P3808

本体是AC自动机的原始版,要求统计有多少模式串是文本串中出现过,代码如下

#include<bits/stdc++.h>
using namespace std;

//一些习惯的简化
#define el '\n'
#define rep(i, a, b) for(int i = (a); i <= (b); i++)
#define lop(i, a, b) for(int i = (a); i <  (b); i++)
#define dwn(i, a, b) for(int i = (a); i >= (b); i--)
#define ms(a, b) memset(a, b, sizeof(a))

typedef long long LL;
typedef pair<int,int> PII;
typedef pair<LL, LL> PLL;
const int MAXN = 1e7 + 1000;
const int INF = 0x3f3f3f3f;
const int SIZE = 26 + 10;
const LL LNF = 9223372036854775807;
int ch[MAXN][SIZE];     //用于储存字典树,代表一个字符的地址
bool vis[MAXN];         //最后计数时记录遍历访问情况
int val[MAXN];          //记录某模式串的结束结点
LL ans = 0;

int idx(char c){ return c - 'a'; };//把字幕'a' - 'z' 转换为 0 - 25
//建立字典树
struct Trie{
    int pos;            //新的存储结点的位置
    Trie(){pos = 1; ms(ch[0], 0); ms(vis, false);};//创建新树的初始化
    void insert(string T){//插入字典树中
        int Tlen = T.length(), p = 0;
        lop(i, 0, Tlen){
            int x = idx(T[i]);
            if(!ch[p][x]){
                ms(ch[pos], 0);
                val[pos] = 0;
                ch[p][x] = pos++;
            }
            p = ch[p][x];
        }
        val[p]++;
    }
};
//Fail指针(用数组表示)的建立与last优化
int last[MAXN], fail[MAXN];
void getFail(){
    queue<int> q;
    fail[0] = 0;//根只能指向自己
    lop(x, 0, 26){//对于根直接相连的结点进行初始化
        int u = ch[0][x];
        if(u){//存在这个结点
            fail[u] = 0;
            last[u] = 0;
            q.push(u);
        }
    }
    //进入类似BFS环节
    while(!q.empty()){
        int u = q.front();
        q.pop();
        lop(x, 0, 26){
            int v = ch[u][x]; //v是u的子结点
            if(!v){//u没有这个子节点    last优化一
                ch[u][x] = ch[fail[u]][x];//这个u直接连向能匹配到x的fail指针的子节点 连不到则回到0
                continue;
            }
            q.push(v);//如果u有这个子结点,入队
            int w = fail[u];
            while(w && !ch[w][x]) w = fail[w];//更新fail[w]找到第一个子节点有v的点  last优化优化二
            fail[v] = ch[w][x];
            last[v] = val[fail[v]] ? fail[v] : last[fail[v]];
        }
    }
}
//正式AC自动机的查找过程
void CountNum(int j){//计数
    if(j && !vis[j]){
        ans += val[j];
        vis[j] = true;
        CountNum(last[j]);//如果j还能找到下一个结束节点,继续搜索
    }
}
void ACautoFind(string S){//自动机
    int Slen = S.length();
    int now = 0;//当前所在位置
    lop(i, 0, Slen){//遍历每个字符
        int x = idx(S[i]);
        now = ch[now][x];
        if(val[now])//now是结束节点
            CountNum(now);
        else if(last[now]){//通过now的last指针可以连到一个结束节点
            CountNum(last[now]);
        }
    }
}

int main(){
    cin.tie(0);
    cout.tie(0);
    cin.sync_with_stdio(false);
    int n;
    string str;
    Trie trie;
    ans = 0;
    cin >> n;   //getchar() + getline(cin, str)过不了!!!
    rep(i, 1, n){
        cin >> str;
        trie.insert(str);   
    }
    getFail();
    cin >> str;
    ACautoFind(str);
    cout << ans << el;
    return 0;
}

P5357 【模板】AC 自动机(二次加强版)

https://www.luogu.com.cn/problem/P5357

作为上一题的加强版本,其实并没有过多的修改,原本因为统计个数所以设置了vis[]数组避免多次统计,而新的题目是要输出出现最多次的模式串,那么我们就应该统计出现次数,所以vis[]的含义从原来的是否出现变成了出现次数。而其次就是增加num[]数组用于标记每个结束结点对应的字符串编号(我们这里设为从1 ~ n)。在记录完以后再遍历输出即可,与第一版没太大差异,只需要改动输出和计数的细节。代码如下

#include<bits/stdc++.h>
using namespace std;

//一些习惯的简化
#define el '\n'
#define rep(i, a, b) for(int i = (a); i <= (b); i++)
#define lop(i, a, b) for(int i = (a); i <  (b); i++)
#define dwn(i, a, b) for(int i = (a); i >= (b); i--)
#define ms(a, b) memset(a, b, sizeof(a))

typedef long long LL;
typedef pair<int,int> PII;
typedef pair<LL, LL> PLL;
const int MAXN = 1e7 + 1000;
const int INF = 0x3f3f3f3f;
const int SIZE = 26 + 10;
const LL LNF = 9223372036854775807;
int ch[MAXN][SIZE];     //用于储存字典树,代表一个字符的地址
int val[MAXN];          //记录某模式串的结束结点
//多加一个处理个数的数组
int cnt = 0;            //记录编号  从1开始
int num[MAXN];          //登记结束结点所对应的子串编号
int vis[MAXN];
LL ans = 0;

int idx(char c){ return c - 'a'; };//把字幕'a' - 'z' 转换为 0 - 25
//建立字典树
struct Trie{
    int pos;            //新的存储结点的位置
    Trie(){     //创建新树的初始化
        cnt = 0; 
        pos = 1; 
        ms(ch[0], 0); 
        ms(vis, 0); 
        ms(num, 0);
    };  
    void insert(string T){//插入字典树中
        int Tlen = T.length(), p = 0;
        lop(i, 0, Tlen){
            int x = idx(T[i]);
            if(!ch[p][x]){
                ms(ch[pos], 0);
                val[pos] = 0;
                ch[p][x] = pos++;
            }
            p = ch[p][x];
        }
        val[p]++;
        num[p] = (++cnt);
    }
};
//Fail指针(用数组表示)的建立与last优化
int last[MAXN], fail[MAXN];
void getFail(){
    queue<int> q;
    fail[0] = 0;//根只能指向自己
    lop(x, 0, 26){//对于根直接相连的结点进行初始化
        int u = ch[0][x];
        if(u){//存在这个结点
            fail[u] = 0;
            last[u] = 0;
            q.push(u);
        }
    }
    //进入类似BFS环节
    while(!q.empty()){
        int u = q.front();
        q.pop();
        lop(x, 0, 26){
            int v = ch[u][x]; //v是u的子结点
            if(!v){//u没有这个子节点    last优化一
                ch[u][x] = ch[fail[u]][x];//这个u直接连向能匹配到x的fail指针的子节点 连不到则回到0
                continue;
            }
            q.push(v);//如果u有这个子结点,入队
            int w = fail[u];
            while(w && !ch[w][x]) w = fail[w];//更新fail[w]找到第一个子节点有v的点  last优化优化二
            fail[v] = ch[w][x];
            last[v] = val[fail[v]] ? fail[v] : last[fail[v]];
        }
    }
}
//正式AC自动机的查找过程
void CountNum(int j){//计数
    if(j){
        vis[num[j]]++;
        CountNum(last[j]);//如果j还能找到下一个结束节点,继续搜索
    }
}
void ACautoFind(string S){//自动机
    int Slen = S.length();
    int now = 0;//当前所在位置
    lop(i, 0, Slen){//遍历每个字符
        int x = idx(S[i]);
        now = ch[now][x];
        if(val[now])//now是结束节点
            CountNum(now);
        else if(last[now]){//通过now的last指针可以连到一个结束节点
            CountNum(last[now]);
        }
    }
}

int main(){
    cin.tie(0);
    cout.tie(0);
    cin.sync_with_stdio(false);
    int n;
    while(cin >> n && n){   //getchar() + getline(cin, str)过不了!!!
        string str[200], T;
        Trie trie;
        ans = 0;
        rep(i, 1, n){
            cin >> str[i];
            trie.insert(str[i]);   
        }
        getFail();
        cin >> T;   //文本串
        ACautoFind(T);
        int maxnum = -1;
        rep(i, 1, n){
            maxnum = max(maxnum, vis[i]);
        }
        cout << maxnum << el;
        rep(i, 1, n){
            if(vis[i] == maxnum)
                cout << str[i] << el;
        }
    }
    return 0;
}

P5357 【模板】AC 自动机(二次加强版)

https://www.luogu.com.cn/problem/P5357

对于这第三个版本我开始看感觉好像比第二个还容易,甚至就直接每个输出就完事了,然后看了下数据,明显数据量加大了不少。于是用原本的代码提交了一发。结果肯定是过不了的(交的时候其实也就猜到了,不然怎么可能叫二次加强呢)

 并且我注意到这题从蓝题变成紫题,相比没那么简单。

 根据题目说给的数据,我们可以发现字典树的最大长度远数组可容纳的范围。于是我就止步于此了(菜鸡就是我,水平就是低)。

如果还要深入学习的大佬可以继续研究一下,大概是优化思路有

        拓扑建图优化  树上差分 领接表优化空间存储 

 当然你也可以或直接改用后缀数组写法(以后可能会讲到)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值