▍题意

给定 nnn 个字符串替换规则 (si,1,si,2)(s_{i,1}, s_{i,2})(si,1,si,2),其中 ∣si,1∣=∣si,2∣|s_{i,1}| = |s_{i,2}|si,1=si,2。对于 qqq 个询问,每个询问给出两个字符串 t1t_1t1t2t_2t2,求有多少种将 t1t_1t1 的某个子串 yyy(等于某个 si,1s_{i,1}si,1)替换为对应的 si,2s_{i,2}si,2 后能得到 t2t_2t2 的方法。两种方法不同当且仅当子串位置不同或使用的规则不同。

数据范围:n,q≤2×105n, q \leq 2 \times 10^5n,q2×105∑∣si,1∣+∣si,2∣≤5×106\sum |s_{i,1}| + |s_{i,2}| \leq 5 \times 10^6si,1+si,25×106∑∣tj,1∣+∣tj,2∣≤5×106\sum |t_{j,1}| + |t_{j,2}| \leq 5 \times 10^6tj,1+tj,25×106


▍分析

思路:构建特殊的 AC 自动机,其中 Trie 树的边以字符对 (c1,c2)(c_1, c_2)(c1,c2) 为标识,同时匹配 t1t_1t1t2t_2t2 的对应字符。

  1. 特殊 Trie 树构建:将每个规则 (s1,s2)(s_1, s_2)(s1,s2) 插入 Trie 树,边标识为 (s1[i],s2[i])(s_1[i], s_2[i])(s1[i],s2[i]) 组成的整数 (s1[i]−′a′)×26+(s2[i]−′a′)(s_1[i]-'a') \times 26 + (s_2[i]-'a')(s1[i]a)×26+(s2[i]a)

  2. AC 自动机构建:在 Trie 树上构建 Fail 指针和 Fail 树,用于快速跳转。

  3. 查询处理:对于每个查询 (t1,t2)(t_1, t_2)(t1,t2)

    • 找到第一个不同位置 lll 和最后一个不同位置 rrr
    • 在 AC 自动机上匹配字符对序列 (t1[i],t2[i])(t_1[i], t_2[i])(t1[i],t2[i])
    • 对于每个位置 i≥r−1i \geq r-1ir1,在 Fail 树上找到满足深度条件的节点,统计规则数量
  4. 优化:使用倍增数组快速在 Fail 树上跳转,预处理每个节点的深度和祖先信息。

时间复杂度O(L1+L2+(n+q)log⁡n)O(L_1 + L_2 + (n+q)\log n)O(L1+L2+(n+q)logn),其中 L1,L2L_1, L_2L1,L2 分别是规则和查询的总长度。


▍参考代码
const int N = 2e5 + 5, M = 5e6 + 5, MM = 25e5;

int n, q, tot, fail[MM], dep[MM];
char s1[M], s2[M];
int tag[MM], fa[MM][22];
unordered_map<int, int> trie[MM]; // 使用哈希表存储Trie树,键为字符对编码
vector<int> g[MM];                // Fail树

// 向Trie树中添加一个替换规则
void add() {
    int now = 0; // 从根节点开始
    for (int i = 1; s1[i]; i++) {
        // 将字符对(s1[i], s2[i])编码为一个整数
        int p = (s1[i] - 'a') * 26 + s2[i] - 'a';
        // 如果当前节点没有这个字符对对应的边,创建新节点
        if (!trie[now][p]) {
            trie[now][p] = ++tot;
            dep[tot] = dep[now] + 1; // 记录节点深度
        }
        now = trie[now][p]; // 移动到下一个节点
    }
    ++tag[now]; // 标记该节点为一个规则的终点
}
// 构建AC自动机的Fail指针
void getfail() {
    queue<int> q;
    q.push(0); // 根节点的Fail指针指向自己
    while (!q.empty()) {
        int x = q.front();
        q.pop();
        // 遍历当前节点的所有子节点
        for (auto pp : trie[x]) {
            int p = pp.first;   // 字符对编码
            int to = pp.second; // 子节点编号
            int now = fail[x];  // 从父节点的Fail指针开始

            // 在Fail链上寻找匹配的节点
            while (now && trie[now].find(p) == trie[now].end())
                now = fail[now];

            // 如果找到匹配,设置子节点的Fail指针
            if (x && trie[now].find(p) != trie[now].end())
                now = trie[now][p];
            fail[to] = now;

            // 构建Fail树
            g[now].push_back(to);
            q.push(to);
        }
    }
}
// DFS遍历Fail树,预处理倍增数组和标记前缀和
void dfs(int x) {
    for (int to : g[x]) {
        tag[to] += tag[x]; // 标记前缀和
        fa[to][0] = x;     // 直接父节点

        // 预处理倍增数组
        for (int i = 1; i <= 21; i++)
            fa[to][i] = fa[fa[to][i - 1]][i - 1];

        dfs(to);
    }
}

int read(char *s) {
    char c;
    int i = 0;
    while ((c = getchar()) < 'a' || c > 'z') continue;
    s[++i] = c;
    while ((c = getchar()) >= 'a' && c <= 'z') s[++i] = c;
    s[i + 1] = 0;
    return i;
}

int main() {
    scanf("%d%d", &n, &q);

    // 读取所有替换规则并构建Trie树
    for (int i = 1; i <= n; i++) 
        read(s1), read(s2), add();

    // 构建AC自动机
    getfail();
    dfs(0);
    // 处理每个查询
    for (int i = 1; i <= q; i++) {
        int len = read(s1), len2 = read(s2);

        // 如果长度不同,直接输出0
        if (len != len2) {
            puts("0");
            continue;
        }

        // 找到第一个和最后一个不同的字符位置
        int l = len + 1, r = 0;
        for (int i = 1; i <= len; i++)
            if (s1[i] != s2[i]) {
                l = i; // 第一个不同位置
                break;
            }
        for (int i = len; i; i--)
            if (s1[i] != s2[i]) {
                r = i; // 最后一个不同位置
                break;
            }
        int now = 0, ans = 0;
        // 在AC自动机上匹配字符对序列
        for (int i = 1; i <= len; i++) {
            int p = (s1[i] - 'a') * 26 + s2[i] - 'a';

            // 在Fail链上寻找匹配
            while (now && trie[now].find(p) == trie[now].end())
                now = fail[now];

            // 如果找到匹配,移动到对应节点
            if (trie[now].find(p) != trie[now].end())
                now = trie[now][p];

            // 当当前位置超过最后一个不同位置时,统计答案
            if (i >= r) {
                // 检查当前节点深度是否足够覆盖不同区间
                if (dep[now] < i - l + 1) continue;

                // 使用倍增找到深度刚好小于(i-l+1)的节点
                int x = now;
                for (int j = 21; j >= 0; j--)
                    if (dep[fa[x][j]] >= i - l + 1)
                        x = fa[x][j];
                x = fa[x][0]; // 移动到第一个深度不足的节点

                // 统计深度在[i-l+1, dep[now]]之间的规则数量
                ans += tag[now] - tag[x];
            }
        }

        printf("%d\n", ans);
    }
    return 0;
}
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐