CSP202312C.树上搜索

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

题意分析

本题要求模拟一个基于二分策略的分类提问过程。给定一棵以 1 为根的树,每个节点代表一个类别并带有一个权重。对于每个查询给定的目标类别 target,需要按照以下规则生成提问序列:

  1. 维护一个候选类别集合,初始包含全部 n 个类别;总权重为所有类别的权重之和。
  2. 对于候选集合中的每个类别 u,计算其子树(在候选集合中)的权重和 sum[u],并计算
    delta = |sum[u] - (total - sum[u])|
    即该类别子树权重与其余部分权重之差的绝对值。
  3. 选择 delta 最小的类别作为本次提问类别(若并列取编号较小者),输出该编号。
  4. 判断目标类别 target 是否在该类别的子树内(根据原始树的祖先关系):
    • 若在,则候选集合缩小为该类别及其后代(删除其余节点);
    • 若不在,则删除该类别及其后代,保留其余节点。
  5. 重复步骤 2-4,直到候选集合中只剩一个类别,停止。

需要输出每次提问的类别编号。

思路

本题数据范围 n ≤ 2000m ≤ 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 及其后代:若 vbest 子树内则删除。
  • 删除节点时,需要更新 totalsumClosure
    • 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 防止溢出,以及并列时选择编号较小的类别。

相关推荐
Figo_Cheung1 小时前
Figo共振网络宇宙演化论(RNC) :从原初对称破缺到全息大和谐的演化路径研究
开发语言·php·量子计算
重生之我来学Python1 小时前
Git-SVN 混合开发,从入门到精通!
开发语言·git·svn·github
CoderYanger1 小时前
前端基础——JavaScript(基础语法)(下篇)
java·开发语言·前端·javascript·程序人生·面试·职场和发展
京师20万禁军教头1 小时前
39面向对象(高级)-设计模式
java·开发语言·设计模式
白山编程大哥2 小时前
Java 迭代器接口详解:从 Iterator 到 ListIterator 的完整指南
java·开发语言·windows
见叶之秋2 小时前
【C++】String类的使用(含模拟实现)
java·开发语言·c++
想吃火锅10052 小时前
【leetcode】42.接雨水js
开发语言·javascript·ecmascript
专注仿真3 小时前
RabbitMQ消息队列实战项目
数据结构·rabbitmq
wuminyu3 小时前
Linux内核线程调度与虚拟线程调度机制系统级深度剖析
java·linux·c语言·jvm·c++