P14363 [CSP-S 2025] 谐音替换

字符串拆核:同核心分组,左边看后缀,右边看前缀,再转成 Trie 上的二维数点。

课堂版 先建模 再看代码

一、先回答问题

问题本质

统计一条规则能否在某个位置把 t1 一次替换成 t2。

容易走偏

只比较第一处和最后一处不同是不够的,中间不同核心必须完整相等。

二、核心观察

把题目变成模型
建模规则和询问都拆成左相同、不同核心、右相同三段;核心必须完全一致。
优化左后缀反转成前缀,右边本来就是前缀;两棵 Trie 的祖先关系用 DFS 序变成矩形覆盖点。
正确性一次替换必须覆盖所有不同位置;覆盖外侧的字符在替换前后相同,所以只能来自左右相同部分。

三、互动演示

--
--
--

四、自己选答案

问题 1

左侧为什么要反转后放进 Trie?

问题 2

若询问两个串长度不同,答案是多少?

五、手推结果

对象左相同核心右相同
规则 xabcx -> xadexxabc -> dex
规则 bc -> debc -> de
询问 xabcx -> xadexxabc -> dex
可匹配数两条规则都满足核心相同答案 2

六、C++14 参考代码

可提交代码
#include <bits/stdc++.h>
using namespace std;

struct Trie {
    struct Edge {
        int to, next;
        unsigned char ch;
    };

    vector<int> head, tin, tout;
    vector<Edge> edges;

    Trie() { head.push_back(-1); }

    int newNode() {
        head.push_back(-1);
        return (int)head.size() - 1;
    }

    int child(int u, int c) const {
        for (int e = head[u]; e != -1; e = edges[e].next) {
            if (edges[e].ch == c) return edges[e].to;
        }
        return -1;
    }

    int addChild(int u, int c) {
        int v = child(u, c);
        if (v != -1) return v;
        v = newNode();
        edges.push_back({v, head[u], (unsigned char)c});
        head[u] = (int)edges.size() - 1;
        return v;
    }

    int insertForward(const string &s, int l, int r) {
        int u = 0;
        for (int i = l; i < r; i++) u = addChild(u, s[i] - 'a');
        return u;
    }

    int insertReversePrefix(const string &s, int r) {
        int u = 0;
        for (int i = r - 1; i >= 0; i--) u = addChild(u, s[i] - 'a');
        return u;
    }

    int walkForward(const string &s, int l, int r) const {
        int u = 0;
        for (int i = l; i < r; i++) {
            int v = child(u, s[i] - 'a');
            if (v == -1) break;
            u = v;
        }
        return u;
    }

    int walkReversePrefix(const string &s, int r) const {
        int u = 0;
        for (int i = r - 1; i >= 0; i--) {
            int v = child(u, s[i] - 'a');
            if (v == -1) break;
            u = v;
        }
        return u;
    }

    void buildDfn() {
        int n = (int)head.size();
        tin.assign(n, 0);
        tout.assign(n, 0);

        vector<pair<int, int>> st;
        st.reserve(n);
        int timer = 0;

        tin[0] = ++timer;
        st.push_back({0, head[0]});
        while (!st.empty()) {
            int u = st.back().first;
            int &it = st.back().second;
            if (it == -1) {
                tout[u] = timer;
                st.pop_back();
                continue;
            }
            int e = it;
            it = edges[e].next;
            int v = edges[e].to;
            tin[v] = ++timer;
            st.push_back({v, head[v]});
        }
    }

    int size() const { return (int)head.size(); }
};

struct Fenwick {
    int n;
    vector<long long> bit;

    Fenwick(int n = 0) { init(n); }

    void init(int n_) {
        n = n_;
        bit.assign(n + 1, 0);
    }

    void add(int x, long long v) {
        for (; x <= n; x += x & -x) bit[x] += v;
    }

    void rangeAdd(int l, int r, long long v) {
        if (l > r) return;
        add(l, v);
        if (r + 1 <= n) add(r + 1, -v);
    }

    long long query(int x) const {
        long long res = 0;
        for (; x > 0; x -= x & -x) res += bit[x];
        return res;
    }
};

struct Record {
    int leftNode, rightNode;
};

struct Ask {
    int leftNode, rightNode, id;
};

struct Event {
    int x, y1, y2, delta;
    bool operator<(const Event &other) const {
        return x < other.x;
    }
};

struct PointAsk {
    int x, y, id;
    bool operator<(const PointAsk &other) const {
        return x < other.x;
    }
};

string makeKey(const string &a, const string &b, int l, int r) {
    int len = r - l + 1;
    string key;
    key.reserve(len * 2 + 1);
    key.append(a, l, len);
    key.push_back('{');
    key.append(b, l, len);
    return key;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, q;
    cin >> n >> q;

    Trie leftTrie, rightTrie;
    unordered_map<string, int> coreId;
    coreId.reserve((n + q) * 2 + 10);
    coreId.max_load_factor(0.7);

    vector<vector<Record>> records;
    vector<vector<Ask>> asks;

    auto getCoreId = [&](const string &key) -> int {
        auto it = coreId.find(key);
        if (it != coreId.end()) return it->second;
        int id = (int)records.size();
        coreId.emplace(key, id);
        records.push_back(vector<Record>());
        asks.push_back(vector<Ask>());
        return id;
    };

    for (int i = 0; i < n; i++) {
        string a, b;
        cin >> a >> b;
        int len = (int)a.size();

        int l = 0;
        while (l < len && a[l] == b[l]) l++;
        if (l == len) continue;

        int r = len - 1;
        while (a[r] == b[r]) r--;

        int id = getCoreId(makeKey(a, b, l, r));
        int leftNode = leftTrie.insertReversePrefix(a, l);
        int rightNode = rightTrie.insertForward(a, r + 1, len);
        records[id].push_back({leftNode, rightNode});
    }

    vector<long long> answer(q, 0);
    for (int qi = 0; qi < q; qi++) {
        string a, b;
        cin >> a >> b;
        if (a.size() != b.size()) continue;

        int len = (int)a.size();
        int l = 0;
        while (l < len && a[l] == b[l]) l++;
        if (l == len) continue;

        int r = len - 1;
        while (a[r] == b[r]) r--;

        string key = makeKey(a, b, l, r);
        auto it = coreId.find(key);
        if (it == coreId.end()) continue;

        int leftNode = leftTrie.walkReversePrefix(a, l);
        int rightNode = rightTrie.walkForward(a, r + 1, len);
        asks[it->second].push_back({leftNode, rightNode, qi});
    }

    leftTrie.buildDfn();
    rightTrie.buildDfn();

    Fenwick bit(rightTrie.size() + 2);

    for (int id = 0; id < (int)records.size(); id++) {
        if (records[id].empty() || asks[id].empty()) continue;

        vector<Event> events;
        events.reserve(records[id].size() * 2);
        for (const Record &rec : records[id]) {
            int lx1 = leftTrie.tin[rec.leftNode];
            int lx2 = leftTrie.tout[rec.leftNode] + 1;
            int ry1 = rightTrie.tin[rec.rightNode];
            int ry2 = rightTrie.tout[rec.rightNode];

            events.push_back({lx1, ry1, ry2, 1});
            events.push_back({lx2, ry1, ry2, -1});
        }
        sort(events.begin(), events.end());

        vector<PointAsk> points;
        points.reserve(asks[id].size());
        for (const Ask &ask : asks[id]) {
            points.push_back({
                leftTrie.tin[ask.leftNode],
                rightTrie.tin[ask.rightNode],
                ask.id
            });
        }
        sort(points.begin(), points.end());

        int ptr = 0;
        for (const PointAsk &ask : points) {
            while (ptr < (int)events.size() && events[ptr].x <= ask.x) {
                bit.rangeAdd(events[ptr].y1, events[ptr].y2, events[ptr].delta);
                ptr++;
            }
            answer[ask.id] = bit.query(ask.y);
        }

        while (ptr < (int)events.size()) {
            bit.rangeAdd(events[ptr].y1, events[ptr].y2, events[ptr].delta);
            ptr++;
        }
    }

    for (int i = 0; i < q; i++) cout << answer[i] << '\n';
    return 0;
}
空左/右相同部分对应 Trie 根节点。
`makeKey` 用不同核心分组。
每组独立做扫描线,避免不同核心互相干扰。