P2414 [NOI2011]阿狸的打字机 · AC自动机 + fail树 + 树状数组

题解

学习来源1 · 大佬博客直通车
学习来源2 · 大佬博客直通车
学习来源3 · 大佬博客直通车

在ac自动机里,如果字符串a可以通过fail指针指向字符串b,那么就说明a串中包含b串

问y串中有多少个x串,等同于问:
y中有多少个节点的fail指针直接或间接指向x的末尾节点

可以看到主要是根据fail进行跳转,
建立ac自动机后,以fail的角度看,其构造形似是一颗树,建立起以fail为关系的树,现在我们把它就做fail树,其性质有:
一个字符串的后缀中有一个字符串等价fail树中一个节点的子树中有另一个节点,也就是看一个点在Trie中到根节点的路径上有多少个属于那个点的Fail树。

再看一遍题目,现在问题等价于:
统计x的子树中来自y的节点的个数,

而实际的答案就是 fail树上x串末尾点的子树与Trie树上x到y的树链的交集中点的个数

由于在一颗树中,一个节点及其子树在DFS序中是连续的一段,那么我们可以用一个树状数组来维护答案

所以,大致步骤如下:
根据ac自动机建立fail树,
dfs求出每个串的起始位置和末尾位置,这将用于树状数组区间求和
保存每个问题对应的串和编号,
再跑一遍trie树,当遍历到代表y串的节点时,查询tire树中 root - y 的路径上,有多少个节点属于fail树中x的子树

我终于会了…


在这里插入图片描述


#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int N = 1e6 + 10;
const int INF = 0x3f3f3f3f;
int n, m;

int pat[N], k = 0;

namespace AC {//AC自动机板子
    const int maxn = 26;

    struct Tree {//字典树
        int fail;//失配指针
        int vis[maxn];//子节点位置
        int end;//标记有几个单词以这个节点
        int father;
    } AC[N];
    int cnt = 0;//编号

    vector<int> Trie[N];//再次构建tire树 用于后面遍历trie树

    void clear(int x) {//清空
        memset(AC[x].vis, 0, sizeof(AC[x].vis));
        AC[x].fail = 0;//结束标志 一般以0为root
        AC[x].end = 0;
    }

    void build(string s) {
        int len = s.length();
        int now = 0;//字典树当前指针
        for (int i = 0; i < len; ++i) {
            if (s[i] == 'B') {
                now = AC[now].father;
            } else if (s[i] == 'P') {
                pat[++k] = now;
            } else {
                if (AC[now].vis[s[i] - 'a'] == 0) {
                    //trie树没有这个子节点
                    AC[now].vis[s[i] - 'a'] = ++cnt;
                    clear(cnt);

                    AC[cnt].father = now;
                    Trie[now].push_back(cnt);
                }
                now = AC[now].vis[s[i] - 'a'];
            }
        }
    }

    queue<int> q;

    void get_fail() {//构造fail指针
        for (int i = 0; i < maxn; ++i) {//特殊处理root
            if (AC[0].vis[i]) {
                AC[AC[0].vis[i]].fail = 0;
                q.push(AC[0].vis[i]);
            }
        }
        while (!q.empty()) {
            int u = q.front();
            q.pop();

            for (int i = 0; i < maxn; ++i) {
                if (AC[u].vis[i]) {//存在子节点
                    AC[AC[u].vis[i]].fail = AC[AC[u].fail].vis[i];
                    //子节点的fail指针 指向父节点fail指针所指向的节点 的相同的子节点 vis[i]相同
                    //如果那个子节点不存在 相当于该子节点fail=0 指向了根节点
                    q.push(AC[u].vis[i]);
                } else {//不存在子节点
                    AC[u].vis[i] = AC[AC[u].fail].vis[i];
                    //当前节点的子节点 指向 当前节点fail指针指向的节点的子节点
                    //把当前节点fail指向的节点 的子节点 作为自己的子节点
                }
            }
        }
    }

    int beg[N], end[N], dfn = 0;
    vector<int> Fail[N];//fail树

    void dfs(int u) {//时间戳
        beg[u] = ++dfn;
        for (int v:Fail[u])
            dfs(v);
        end[u] = ++dfn;
    }

    void fail_tree() {//建fail树 目的是为了可以用dfs求出时间戳
        for (int i = 1; i <= cnt; ++i) {
            Fail[AC[i].fail].push_back(i);
        }
        dfs(0);//root
    }

    void init() {
        cnt = 0;
        clear(0);
    }
}
using namespace AC;

namespace BIT {//树状数组板子
    int bit[N];
    int lowbit(int x) { return x & (-x); }
    void add(int x, int val) {
        for (; x <= N; x += lowbit(x)) {
            bit[x] += val;
        }
    }
    int ask(int x) {
        int res = 0;
        for (; x; x -= lowbit(x)) {
            res += bit[x];
        }
        return res;
    }
}
using namespace BIT;

string s;
vector<pii> query[N];//存问题及编号
int ans[N];//统计答案

void solve(int u) {
    add(AC::beg[u], 1);//开始探索子树时 +1
    for (auto p:query[u]) {
        int id = p.second;//问题编号
        int x = p.first;
        //查询 tire树中 root-y的路径上 有多少个节点 属于fail树中x的子树
        ans[id] = ask(AC::end[x]) - ask(AC::beg[x] - 1);
    }
    /*for (int i = 0; i < AC::maxn; ++i) {
        int v = AC::AC[u].vis[i];
        if (v) {
            solve(v);
        }
    } //这样写是错误的 求fail指针的时候 如果没有儿子节点会造成各种诡异的循环*/
    for (auto v:Trie[u])
        solve(v);

    add(AC::beg[u], -1);//结束时-1
}

int main() {
    ios::sync_with_stdio(0);

    init();

    cin >> s;
    build(s);//建树
    get_fail();//构造fail指针
    fail_tree();//建fail树

    cin >> m;
    for (int i = 1, x, y; i <= m; ++i) {
        cin >> x >> y;
        query[pat[y]].push_back({pat[x], i});
    }
    solve(0);//从根节点跑一遍trie树统计答案
    for (int i = 1; i <= m; ++i) {
        cout << ans[i] << endl;
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值