AC自动机

  • AC 自动机= Trie + KMP
  • AC 自动机基于Trie,将KMP 的Border 概念推广到多模式串上。
  • AC 自动机是一种离线型数据结构,即不支持增量添加新的字符串。
  • AC 自动机常用于将字符串询问类的问题进行离线处理,也经常与各种
    DP 结合,或是补全成Trie 图。

广义border

1.推广到两个串:对于两个串S 和T,相等的p 长度的S 的后缀和T 的前缀称为一个border。

2.推广到一个字典:对于串S 和一个字典D,相等的p 长度的S 的后缀,和任意一个字典串T 的前缀称为一个border。

3.失配(Fail)指针: 对于Trie 中的每一个节点(即某个字典串的前缀),它与Trie 中所有串的最大Border 即为失配指针

ac自动机维护的是当前状态的最大后缀满足这个后缀是字典树的前缀

struct AC {
    static constexpr int asz = 26;
    struct Node {       // 表示Trie树的一个节点
        int len;        // 节点对应的字符串的长度
        int fail;       // 节点的后缀链接
        int mxsuf = 0;  // 后缀的恰好匹配一个完整的字典串
        bool vis = 0;   // 这个状态是不是存在子串是字典串
        array<int, asz> next;
        // 表示从当前节点到下一个节点的转换,数组大小为字母表大小
        Node() : len{0}, fail{0}, next{} {}
    };

    vector<Node> t;
    AC() { init(); }

    void init() {
        t.assign(2, Node());  // 创建两个节点,分别是根节点和伪根节点
        t[0].next.fill(1);    // 将根节点的所有next指向伪根节点
        t[0].len = -1;        // 设置根节点的len为-1
    }

    int newNode() {        // 创建一个新节点,并返回其索引
        t.emplace_back();  // 向向量t中添加一个新的Node节点
        return t.size() - 1;
    }

    int add(const string &a, char offset = 'a') {  // 向Trie树中添加字符串
        int p = 1;                                 // 从伪根节点开始
        for (auto c : a) {
            int x = c - offset;
            if (t[p].next[x] == 0) {
                t[p].next[x] = newNode();  // 创建新节点,并更新next数组
                t[t[p].next[x]].len = t[p].len + 1;
            }
            p = t[p].next[x];  // 移动到下一个节点
        }
        t[p].vis = 1;
        t[p].mxsuf = a.size();
        return p;  // 返回最后一个字符对应的节点索引
    }

    void work() {  // 构建AC自动机的后缀链接
        queue<int> q;
        q.push(1);

        while (!q.empty()) {
            int x = q.front();
            q.pop();
            t[x].vis |= t[t[x].fail].vis;
            t[x].mxsuf = max(t[x].mxsuf, t[t[x].fail].mxsuf);
            for (int i = 0; i < asz; i++) {
                if (t[x].next[i] == 0) {                  // 如果当前节点没有对应字符的转移
                    t[x].next[i] = t[t[x].fail].next[i];  // 设置为后缀链接节点的对应转移
                } else {
                    t[t[x].next[i]].fail = t[t[x].fail].next[i];  // 设置新节点的后缀链接
                    q.push(t[x].next[i]);
                }
            }
        }
    }
    int next(int p, int x) { return t[p].next[x]; }
    int next(int p, char x, char offset = 'a') { return t[p].next[(x - offset)]; }
    int fail(int p) { return t[p].fail; }  // 获取节点p的后缀链接
    int len(int p) { return t[p].len; }
    int mxsuf(int p) { return t[p].mxsuf; }  // 获取节点p的匹配的最长后缀,满足他可以与字典串的前缀
    bool vis(int p) { return t[p].vis; }
    int size() { return t.size(); }  // 获取Trie树的节点总数
};

板子:给你一个文本串 $S$ 和 $n$ 个模式串 $T_{1 \sim n}$,请你分别求出每个模式串 $T_i$ 在 $S$ 中出现的次数。

Sol:将文本串放在AC 自动机上运行,求出每个前缀匹配到AC 自动机的哪
个节点,将该节点的标记值+1。这里标记的是匹配串,下面标记的是模式串
每个字典串的出现次数等于失配树的子树内的标记总量。因此;建出fail树,在失配树上自底向上dfs推标记即可。

void solve() {
    AC ac;
    cin >> n;
    vector<int> id(n + 1);
    for (int i = 1; i <= n; i++) {
        string s;
        cin >> s;
        id[i] = ac.add(s);
    }

    ac.work();
    string tt;
    int p = 1;
    cin >> tt;
    int tot = ac.size();
    vector<int> sz(tot);

    m = tt.size();
    for (int i = 0; i < m; i++) {
        int ch = tt[i] - 'a';
        p = ac.next(p, ch);
        sz[p] += 1;
        deb(p);
    }
    vector<vector<int>> e(tot);
    for (int i = 2; i < tot; i++) {
        deb(i, ac.fail(i));
        e[ac.fail(i)].push_back(i);
    }
    auto dfs = [&](auto self, int u) -> void {
        for (auto v : e[u]) {
            self(self, v);
            sz[u] += sz[v];
        }
    };
    dfs(dfs, 1);
    for (int i = 1; i <= n; i++) cout << sz[id[i]] << endl;
}

2。https://ac.nowcoder.com/acm/contest/29086/B

  • 给出一个有N 个串的字典,然后进行M 次操作:
    1. 修改操作:向字典中新增一个字典串S。
    2. 查询操作:查询所有字典串在询问串T 中的出现次数,同一个字典
      串重复出现算多次。
      1 ≤ N, M ≤ 100, 000, 输入字符串总长度不超过3000, 000。

Sol:AC自动机无法在线处理,所以考虑离线询问。先无视修改操作,只考虑查询操作:可以建出AC 自动机。

  • 然后将查询串在AC 自动机上运行,匹配到AC 自动机的一个节点时,将该节点到根的路径上终止标记个数计入答案。

  • 由于AC 自动机是离线型数据结构,即不能支持快速增删字典串,因此
    考虑到修改操作需要先离线,动态维护终止标记,使用dfs 序+ 树状数
    组快速维护每个点到根路径上的终止标记个数。

    这里说一下怎么维护的?我们需要查询的是查询串在AC自动机上走的时候faill链上有多少标记?但是这样直接做不好做,我们考虑每个终止标记只会贡献它的fail子树,所以标记字典串完成区间加,而查询是单点查。正好符合dfs序+树状数组

    debug:由于在跑ac自动机的时候字母和数字映射内部没写,外部也没有处理。现在板子加了offset,减小出错概率。不过也要惊醒这个点

    void solve() {
        int n, m;
        AC ac;
        cin >> n >> m;
        vector<int> pos;
        for (int i = 1; i <= n; i++) {
            string s;
            cin >> s;
            pos.push_back(ac.add(s));
        }
        vector<pii> q(m);
        vector<int> id(m);
        for (int i = 0; i < m; i++) {
            int x;
            string y;
            cin >> x >> y;
            q[i].fs = x, q[i].sec = y;
            if (x == 1)
                id[i] = ac.add(y);
        }
        ac.work();
        int tot = ac.size() - 1;
        vector<vector<int>> e(tot + 1);
        for (int i = 2; i <= tot; i++) e[ac.fail(i)].push_back(i);
        int idx = 0;
        vector<int> l(tot + 1), r(tot + 1);
        auto dfs = [&](auto self, int u) -> void {
            l[u] = ++idx;
            for (auto v : e[u]) {
                self(self, v);
            }
            r[u] = idx;
        };
        dfs(dfs, 1);
        Fwk<int> fw(tot + 2);
        for (auto x : pos) {
            deb(l[x], r[x]);
            fw.add(l[x], 1);
            fw.add(r[x] + 1, -1);
        }
        for (int i = 0; i < m; i++) {
            auto [x, y] = q[i];
            if (x == 1) {
                fw.add(l[id[i]], 1);
                fw.add(r[id[i]] + 1, -1);
            } else {
                int p = 1;
                int res = 0;
                for (auto it : y) {
                    p = ac.next(p, it-'a'), res += fw.sum(l[p]);
                }
                cout << res << endl;
            }
        }
    }
    

    $\sum_{v是u的祖先}(i-len(v)+1) \times (len(询问串)-i+1) $

3.给出一个字典,至多包含300, 000 个字典串,且字典串总长度不超过
300, 000。
再给出一个文本串T(|T| ≤ 300, 000),问最少使用多少个字典串可以拼
接出T。同一个字典串使用多次算多次。
拼接时允许重叠,只需要保证重叠部分匹配即可,例如T = abcd,可以
使用字典串S = cdxx 拼接成T′ = abcdxx。

Sol:首先比较容易想到使用DP:f[i] 表示拼接出T[1, i] 的操作次数.
转移时,设某字典串Si 等于T[1, i] 的后缀,会导致如下转移:
$f[i] = min(f[i − 1], f[i − 2], · · · , f[i − |Si|]]) + 1$。因此只需要寻找满足长度最长的字典串恰好能和当前后缀完美匹配,我们可以选择只让后缀的子后缀匹配,所以是区间转移,用线段树辅助进行DP 转移。
复杂度O(300000 · (26 + log n))。

struct Info {
    int mx = inf;
};
Info operator+(const Info &a, const Info &b) {
    return {min(a.mx, b.mx)};
}
void solve() {
    cin >> n;
    AC ac;
    string s;
    cin >> s;
    for (int i = 1; i <= n; i++) {
        string t;
        cin >> t;
        ac.add(t);
    }
    ac.work();
    s = " " + s;
    int len = s.size() - 1;
    vector<int> dp(len + 1, inf);
    SegmentTree<Info> seg(len + 1);
    int p = 1;
    dp[0] = 0;
    seg.modify(0, {dp[0]});
    deb(inf);
    for (int i = 1; i <= len; i++) {
        p = ac.next(p, s[i] - 'a');
        int tmp = ac.mxsuf(p);
        deb(tmp);
        if (tmp == 0)
            continue;
        int l = max(0, i - tmp);
        dp[i] = min(dp[i], seg.rangeQuery(l, i).mx + 1);
        seg.modify(i, {dp[i]});
    }
    if (dp[len] == inf)
        cout << -1 << endl;
    else
        cout << dp[len] << endl;
}

4.给出一个字典,至多包含60 个字典串,且字典串总长度不超过100。
求所有长度为M(≤ 100) 的串中(共$26^M$ 个),有多少个串至少包含一
个字典串

Sol:考虑容斥,答案= $26^M$- 完全不包含字典串的串个数。
包含与不包含本质上都是字符串匹配问题,因此本题为字符串多模式匹
配,很容易想到利用AC 自动机。
设$f[len][u] $表示长度为len 的字符串,当前匹配到了AC 自动机的u 节
点的方案数。
转移时枚举下一个字符,然后找到匹配的AC 自动机节点v(这一步的复
杂度为O(depth)),如果v 是一个终止节点,则是一个不合法的转移。

复杂度O(M · 100 · 100 · 26) =$ 2.7 · 10^7$

加强长度为M=10000

  • Trie 图
    用上题的做法复杂度高达$2.7 · 10^9 $不能通过。因此需要进行优化:
    可以比较容易的观察到,可以预处理出$trans[u][ch]$ =AC 自动机节点u,后
    面添加字符ch 后会转移到哪个节点。如此可以省掉跳Fail 链的复杂度。
    求$trans[u][ch] $时,讨论:
    • 如果u 节点有ch 后继边,则$trans[u][ch] = nxt[u][ch]$。
    • 否则$,trans[u][ch] = trans[Fail[u]][ch]$。
      因此trans 数组也需要使用bfs 来求,可以和AC 自动机的构造放在一起进行

实现:由于为了卡做法,本题取模卡常,需要用取模类

using i64 = long long;
using u64 = unsigned long long;
using f64 = double;
 
template <class T> constexpr T power(T a, i64 n) {
    T res{1};
    for (; n > 0; n /= 2, a *= a) {
        if (n % 2 == 1) {
            res *= a;
        }
    }
    return res;
}
 
template <i64 P> constexpr i64 mul(i64 a, i64 b) {
    i64 res = a * b - i64(1.0L * a * b / P - 0.5L) * P;
    return res % P;
}
 
template <i64 P> struct ModInt {
    static i64 M;
    constexpr static void setMod(i64 m) { ModInt::M = m; }
    constexpr static i64 getMod() {
        if (P > 0) {
            return P;
        } else {
            return M;
        }
    }
    i64 x;
    constexpr i64 norm(i64 x) const {
        if (x < 0) {
            x += getMod();
        } else if (x >= getMod()) {
            x -= getMod();
        }
        return x;
    }
    constexpr ModInt() : x{} {}
    constexpr ModInt(i64 x) : x{norm(x % getMod())} {}
    constexpr i64 val() const { return x; }
    explicit constexpr operator i64() const { return x; }
    constexpr ModInt operator-() const { return ModInt(getMod() - x); }
    constexpr ModInt inv() const {
        assert(x != 0);
        return power(*this, getMod() - 2);
    }
    constexpr ModInt &operator+=(const ModInt &rhs) {
        x = norm(x + rhs.x);
        return *this;
    }
    constexpr ModInt &operator-=(const ModInt &rhs) {
        x = norm(x - rhs.x);
        return *this;
    }
    constexpr ModInt &operator*=(const ModInt &rhs) {
        if (getMod() < (1LL << 31)) {
            x = 1LL * x * rhs.x % P;
        } else {
            x = mul<getMod()>(x, rhs.x);
        }
        return *this;
    }
    constexpr ModInt &operator/=(const ModInt &rhs) { return *this *= rhs.inv(); }
    friend constexpr ModInt operator+(ModInt lhs, const ModInt &rhs) { return lhs += rhs; }
    friend constexpr ModInt operator-(ModInt lhs, const ModInt &rhs) { return lhs -= rhs; }
    friend constexpr ModInt operator*(ModInt lhs, const ModInt &rhs) { return lhs *= rhs; }
    friend constexpr ModInt operator/(ModInt lhs, const ModInt &rhs) { return lhs /= rhs; }
    friend constexpr bool operator<(const ModInt &lhs, const ModInt &rhs) { return lhs.val() < rhs.val(); }
    friend constexpr bool operator==(const ModInt &lhs, const ModInt &rhs) { return lhs.val() == rhs.val(); }
    friend constexpr bool operator!=(const ModInt &lhs, const ModInt &rhs) { return lhs.val() != rhs.val(); }
    friend istream &operator>>(istream &is, ModInt &rhs) {
        i64 x;
        is >> x;
        rhs = x;
        return is;
    }
    friend ostream &operator<<(ostream &os, const ModInt &rhs) { return os << rhs.val(); }
};
 
template <> i64 ModInt<0>::M = 1000000007;
 
template <i64 V, i64 P> constexpr ModInt<P> CInv = ModInt<P>(V).inv();
 
constexpr int P = 10007;
using Z = ModInt<P>;

考虑对于一个字典串的终止节点,它的fail树子树都会被染成非法节点,所以dfs去完成染色是容易的。但为了减小常数,我们可以在ac自动机的构建过程去维护vis表示是不是包含

 int add(const string &a) {  // 向Trie树中添加字符串
        int p = 1;              // 从伪根节点开始
        for (auto c : a) {
            int x = c - 'A';
            if (t[p].next[x] == 0) {
                t[p].next[x] = newNode();  // 创建新节点,并更新next数组
                t[t[p].next[x]].len = t[p].len + 1;
            }
            p = t[p].next[x];  // 移动到下一个节点
        }
        t[p].vis = 1;
        t[p].mxsuf = a.size();
        return p;  // 返回最后一个字符对应的节点索引
    }
 void work() {  // 构建AC自动机的后缀链接
        queue<int> q;
        q.push(1);

        while (!q.empty()) {
            int x = q.front();
            q.pop();
            t[x].mxsuf = max(t[x].mxsuf, t[t[x].fail].mxsuf);
              t[x].vis |= t[t[x].fail].vis;
            for (int i = 0; i < asz; i++) {
                if (t[x].next[i] == 0) {                  // 如果当前节点没有对应字符的转移
                    t[x].next[i] = t[t[x].fail].next[i];  // 设置为后缀链接节点的对应转移
                } else {
                    t[t[x].next[i]].fail = t[t[x].fail].next[i];  // 设置新节点的后缀链接
//                     t[t[x].next[i]].vis |= t[t[t[x].fail].next[i]].vis;
                    // t[t[x].next[i]].ended |= t[t[t[x].link].next[i]].ended;
                    q.push(t[x].next[i]);
                }
            }
        }
    }

考虑每层的长度的转移是互相不牵连的,用滚动数组dp.本题在ac自动机上转移主要关注的是vis标记的躲避,与具体最长border无关。这里的ac自动机节点看作一个状态。

void solve() {
    AC ac;
    int n, m;
    cin >> n >> m;
    for (int i = 0; i < n; i++) {
        string s;
        cin >> s;
        ac.add(s);
    }
    ac.work();
    int tot = ac.size() - 1;
    vector<Z> dp(tot + 1);
    dp[1] = 1;
    for (int i = 1; i <= m; i++) {
        vector<Z> ndp(tot + 1);
        for (int p = 1; p <= tot; p++) {
            for (int x = 0; x < 26; x++) {
                int v = ac.next(p, x);
                if (ac.vis(v))
                    continue;
                ndp[v] += dp[p];
            }
        }
        dp = move(ndp);
        // dp = move(ndp); 的核心作用是通过移动语义避免拷贝,
        // 从而提高性能。ndp 在执行这句代码后可能会变为空或处于未定义状态,因此后续不要再直接使用 ndp。
    }
    Z ans = 0;

    for (int i = 1; i <= tot; i++) {
        ans += dp[i];
    }
     cout << power(Z(26), m) - ans << "\n";
}

6.给出N ≤ 2, 000 个01 串,它们每一个表示一段病毒序列,病毒代码总
长度不超过30, 000。
求是否存在无限长度的01 串,不包含任意病毒序列作为子串。

Sol:本题本质是问:Trie 图上(去掉所有终止节点)是否存在无限长度的路
径,充要条件为:有环。
因此本题需要先构造AC 自动机,然后补全成为Trie 图,找到所有包含病毒序列的节点。使用拓扑排序或者dfs判定是否有环。

  • 注意,这个环必须是root 节点可达的。
    复杂度O(30, 000 · 2)。
void solve() {
    int n;cin >> n;
    AC ac;
    for (int i = 1; i <= n; i++) {string s;cin >> s;ac.add(s);}
    ac.work();
    int tot = ac.size() - 1;
    vector<vector<int>> e(tot + 1);
    for (int i = 1; i <= tot; i++) {
        for (int j = 0; j < 2; j++) {
            int v = ac.next(i, j);
            if (ac.vis(i))continue;
            e[i].push_back(v);
        }
    }
    vector<int> st(tot + 1);
    auto dfs = [&](auto self, int u) {
        st[u] = 1;
        for (auto v : e[u]) {
            if (st[v] == 2)continue;
            if (st[v] == 1)return true;
            if (self(self, v))return true;
        }
        st[u] = 2;
        return false;
    };
    bool flag = dfs(dfs, 1);
    if (flag)cout << "TAK";
    else cout << "NIE";
}

8.CF547E给出N 个字符串,设f(S, T) 表示T 在S 中的出现次数。
给出Q 次询问:给定L, R, K,求$\sum_{i=L}^{i\leq R} f(S_{i}, S_{K})$

本题做法很多,其中一种做法是《AC 自动机Fail 树Dfs 序上建可持久
化线段树》。

  • 先建出AC 自动机,在回答询问的时候先找到$S_{K}$所在的节点u,那么
    完整包含字符串$S_{K}$ 的状态节点一定位于u 的Fail 树子树内。
    即问题等价于询问u 的Fail 树子树内,$S_{L}$, $S_{L+1}$, · · · , $S_{R}$ 的前缀节点个
    数有多少。
    考虑子树求和,比较容易转化为Dfs 序的区间求和,对字典串的区间询问,容易转化成持久化数据结构。即Dfs 序上建可持久化线段树。

具体来说对每个字符串都会在前缀结点上标记加1,我们去结点的dfs序对应位置上加1,这每一次修改都需要动态开点我们都可以认为这是一个的新版本,但我们需要的是每个字符串之间的版本的权值数量差距。

AC ac;
void solve() {
    int n, m;
    cin >> n >> m;
    vector<int> pos(n + 1);
    vector<string> s(n + 1);
    for (int i = 1; i <= n; i++) {
        cin >> s[i];
        pos[i] = ac.add(s[i]);
    }
    ac.work();
    int tot = ac.size() - 1;
       
    vector<vector<int>> e(tot + 1);
    for (int i = 2; i <= tot; i++) {
        e[ac.fail(i)].push_back(i);
    }
    deb(e);
    vector<int> in(tot + 1), out(tot + 1);
    int idx = 0;
    auto dfs = [&](auto self, int u) -> void {
        deb(u);
        in[u] = ++idx;
        for (auto v : e[u]) {
            self(self, v);
        }
        out[u] = idx;
    };

    dfs(dfs, 1);
    Segment<Info> seg(tot + 5);
    vector<int> ver{0};
    for (int i = 1; i <= n; i++) {
        int prev = ver.back();
        for (int p = 1; auto c : s[i]) {
            p = ac.next(p, c);
            prev = seg.modify(prev, in[p], {1});  // 每次都有当前字符串长度的修改
            // 一个版本需要开len*log(len)的新点
        }
        ver.push_back(prev);  // 以一个字符串为一个版本
    }

    for (int i = 1; i <= m; i++) {
        int l, r, k;
        cin >> l >> r >> k;
        Info res = seg.rangeQuery(ver[l-1], ver[r], in[pos[k]], out[pos[k]] + 1);
        cout << res.x << endl;
    }
}

考虑使用轻量级的dfs序配合树状数组。离线询问,将询问差分,只要求k在前缀L-1中出现次数和求k在前缀R中出现次数。

将询问挂在这两个位置,顺序扫描字符串。每次将当前字典串上的前缀节点加1,然后处理当前节点挂的多个询问,注意加个op表示是加还是减,对于查询就是对当前k的fail树子树内求和

code:


10.https://www.luogu.com.cn/problem/P2414[NOI2011] 阿狸的打字机

题意:给出一颗字典树,多次查询第x个字符串在第y个字符串出现了多少次?

Sol:首先题目以特定的当时生成字典树,仔细思考发现我们不能把所有构成的字符串存下来,也不能每次从头开始走路,而是要动态维护一个p指针和字典树节点的父亲来完成建树。

 struct Node {       // 表示Trie树的一个节点
        int len;        // 节点对应的字符串的长度
        int fail;       // 节点的后缀链接
        int mxsuf = 0;  // 后缀的恰好匹配一个完整的字典串
        bool vis = 0;   // 这个状态是不是存在子串是字典串
        array<int, asz> next;
        int fa = 1;//字典树的父亲
        // 表示从当前节点到下一个节点的转换,数组大小为字母表大小
        Node() : len{0}, fail{0}, next{} {}
    }; 

int add(int p, char c, char offset = 'a') {  // 向Trie树中添加字符串                                      
        int x = c - offset;
        if (t[p].next[x] == 0) {
            t[p].next[x] = newNode();  // 创建新节点,并更新next数组
            t[t[p].next[x]].len = t[p].len + 1;
            t[t[p].next[x]].fa = p;
        }
        p = t[p].next[x];  // 移动到下一个节点
        return p;  // 返回最后一个字符对应的节点索引
    }

考虑问题本身:x在y中出现了多少次,就是x的fail树子树内有多少个y的字典树前缀结点。我们需要子树求和,考虑dfs序加树状数组完成这个操作。

  • 把询问挂到y上,dfs字典树动态维护标记,进去的时候加1,出来的时候减1,这样能保证每次只查单串的贡献,每次退出递归的时候判一下这个点上有没有询问。

细节:要记录pos数组表示第x个字符串的结尾节点编号。字典树dfs由于建的是trie图所以我们需要判一下长度来完成正确的字典树遍历。

AC ac;
void solve() {
    string s;cin >> s;string tmp;
    vector<int> pos; pos.push_back(-1);
    int n = 0,p = 1;
    for (auto x : s) {
        if (x == 'B') {
            p = ac.fa(p);
        } else if (x == 'P') {
            n++;
            pos.push_back(p);
        } else
            p = ac.add(p, x);
    }

    //------------------------------------//
    ac.work();
    int tot = ac.size() - 1;
    vector<bool> vis(tot + 1);
    for (int i = 1; i <= n; i++) vis[pos[i]] = true;
    //-----------------------------------------
    int m;
    cin >> m;
    vector<vector<pii>> q(tot + 1);
    for (int i = 1; i <= m; i++) {
        int x, y;
        cin >> x >> y;
        q[pos[y]].push_back({pos[x], i});
    }
    vector<int> ans(m + 1);
    //-----------------------------------------------
    vector<vector<int>> e(tot + 1);
    for (int i = 2; i <= tot; i++) e[ac.fail(i)].push_back(i);
    vector<int> l(tot + 1), r(tot + 1);
    int idx = 0;
    auto dfs = [&](auto self, int u) -> void {
        l[u] = ++idx;
        for (auto v : e[u]) {
            self(self, v);
        }
        r[u] = idx;
    };
    dfs(dfs, 1);
    //-------------------------------------------------
    Fwk<int> fw(tot + 1);
    auto cal = [&](auto self, int u) -> void {
        fw.add(l[u], 1);
        for (int i = 0; i < 26; i++) {
            int v = ac.next(u, i);
            if (ac.len(v) == ac.len(u) + 1) {
                self(self, v);
            }
        }
        if (vis[u]) {
            for (auto [x, id] : q[u]) {
                ans[id] = fw.rangeSum(l[x] - 1, r[x]);
            }
        }
        fw.add(l[u], -1);
    };
    cal(cal, 1);
    for (int i = 1; i <= m; i++) cout << ans[i] << endl;
}