统计一条规则能否在某个位置把 t1 一次替换成 t2。
P14363 [CSP-S 2025] 谐音替换
字符串拆核:同核心分组,左边看后缀,右边看前缀,再转成 Trie 上的二维数点。
一、先回答问题
只比较第一处和最后一处不同是不够的,中间不同核心必须完整相等。
二、核心观察
建模规则和询问都拆成左相同、不同核心、右相同三段;核心必须完全一致。
优化左后缀反转成前缀,右边本来就是前缀;两棵 Trie 的祖先关系用 DFS 序变成矩形覆盖点。
正确性一次替换必须覆盖所有不同位置;覆盖外侧的字符在替换前后相同,所以只能来自左右相同部分。
三、互动演示
--
--
--
1:准备开始。
四、自己选答案
左侧为什么要反转后放进 Trie?
先自己判断,再点选项。
若询问两个串长度不同,答案是多少?
先自己判断,再点选项。
五、手推结果
| 对象 | 左相同 | 核心 | 右相同 |
|---|---|---|---|
| 规则 xabcx -> xadex | xa | bc -> de | x |
| 规则 bc -> de | 空 | bc -> de | 空 |
| 询问 xabcx -> xadex | xa | bc -> de | x |
| 可匹配数 | 两条规则都满足 | 核心相同 | 答案 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` 用不同核心分组。
每组独立做扫描线,避免不同核心互相干扰。