今天我们来看CSP202312C.树上搜索这道题目



题意分析
本题要求模拟一个基于二分策略的分类提问过程。给定一棵以 1 为根的树,每个节点代表一个类别并带有一个权重。对于每个查询给定的目标类别 target,需要按照以下规则生成提问序列:
- 维护一个候选类别集合,初始包含全部 n 个类别;总权重为所有类别的权重之和。
- 对于候选集合中的每个类别
u,计算其子树(在候选集合中)的权重和sum[u],并计算
delta = |sum[u] - (total - sum[u])|
即该类别子树权重与其余部分权重之差的绝对值。 - 选择
delta最小的类别作为本次提问类别(若并列取编号较小者),输出该编号。 - 判断目标类别
target是否在该类别的子树内(根据原始树的祖先关系):- 若在,则候选集合缩小为该类别及其后代(删除其余节点);
- 若不在,则删除该类别及其后代,保留其余节点。
- 重复步骤 2-4,直到候选集合中只剩一个类别,停止。
需要输出每次提问的类别编号。
思路
本题数据范围 n ≤ 2000,m ≤ 100,允许 O(n²) 级别的查询模拟。
预处理:
- 读入权重、父子关系,建树。
- 进行一次 DFS,得到每个节点的 DFS 序区间
[tin, tout],用于 O(1) 判断节点之间的祖先关系;同时计算每棵子树的原始权重和subSum[u]。
模拟一次查询:
- 使用数组
sumClosure[u]表示当前候选集合中,以u为根的子树内(仅限仍在候选集合中的节点)的权重和。初始时等于subSum[u]。 - 维护变量
total表示当前候选集合的总权重,初始为subSum[1]。 - 候选集合用
vector<int> cand存储所有还在候选中的类别编号。
循环直到 cand.size() == 1:
- 遍历
cand中每个节点u,计算delta = abs(2 * sumClosure[u] - total),选出最优提问节点best。 - 将
best加入答案数组。 - 判断
target是否在best的子树中:利用 DFS 序,inSubtree(best, target)返回tin[best] <= tin[target] && tout[target] <= tout[best]。 - 根据回答缩小候选集合:
- 若回答"是",保留
best及其后代:对于当前cand中每个节点v,若v不在best子树内则删除。 - 若回答"否",删除
best及其后代:若v在best子树内则删除。
- 若回答"是",保留
- 删除节点时,需要更新
total和sumClosure:total -= w[v];- 对于
v的所有祖先(沿着父指针向上直到根),将它们的sumClosure减去w[v],因为这些祖先的"候选子树和"不再包含被删除的节点。
- 更新候选集合
cand为保留的节点。
注意:
由于删除节点时更新祖先的 sumClosure,下一轮计算中 sumClosure[u] 即为当前候选集合中 u 子树内的权重和,符合题意。
时间复杂度:
每个查询最多进行 n-1 次提问,每次扫描候选集合 O(n),并更新被删除节点的祖先 O(depth),最坏 O(n²)。对于 n≤2000,m≤100,总时间在可接受范围内。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int n, m;
cin >> n >> m;
vector<long long> w(n + 1);
for (int i = 1; i <= n; ++i) cin >> w[i];
vector<int> parent(n + 1, 0);
vector<vector<int>> children(n + 1);
for (int i = 2; i <= n; ++i) {
cin >> parent[i];
children[parent[i]].push_back(i);
}
// DFS 序,用于判断祖先关系
vector<int> tin(n + 1), tout(n + 1);
vector<long long> subSum(n + 1, 0);
int timer = 0;
function<void(int)> dfs = [&](int u) {
tin[u] = ++timer;
subSum[u] = w[u];
for (int v : children[u]) {
dfs(v);
subSum[u] += subSum[v];
}
tout[u] = timer;
};
if (n >= 1) dfs(1);
auto inSubtree = [&](int u, int v) {
return tin[u] <= tin[v] && tout[v] <= tout[u];
};
for (int q = 0; q < m; ++q) {
int target;
cin >> target;
vector<long long> sumClosure = subSum;
long long total = subSum[1];
vector<int> cand;
cand.reserve(n);
for (int i = 1; i <= n; ++i) cand.push_back(i);
vector<int> ans;
ans.reserve(n);
while (cand.size() > 1) {
int best = -1;
long long bestDelta = LLONG_MAX;
// 选择 wδ 最小的类别
for (int u : cand) {
long long delta = 2 * sumClosure[u] - total;
if (delta < 0) delta = -delta;
if (delta < bestDelta || (delta == bestDelta && u < best)) {
bestDelta = delta;
best = u;
}
}
ans.push_back(best);
// 判断目标类别是否在 best 的子树中
bool answerYes = inSubtree(best, target);
vector<int> newCand;
newCand.reserve(cand.size());
for (int v : cand) {
bool keep;
if (answerYes) {
keep = inSubtree(best, v);
} else {
keep = !inSubtree(best, v);
}
if (keep) {
newCand.push_back(v);
} else {
// 删除节点 v,更新 total 和所有祖先的 sumClosure
total -= w[v];
int u = v;
while (u != 0) {
sumClosure[u] -= w[v];
u = parent[u];
}
}
}
cand.swap(newCand);
}
for (size_t i = 0; i < ans.size(); ++i) {
if (i) cout << ' ';
cout << ans[i];
}
cout << '\n';
}
return 0;
}
总结
本题核心在于理解二分提问的决策过程,并高效维护动态变化的候选集合及其子树权重和。通过 DFS 序快速判断祖先关系,利用父指针链更新权重,避免了重复计算,使得单次查询的复杂度可以接受。代码实现时需注意数据范围使用 long long 防止溢出,以及并列时选择编号较小的类别。