树上搜索

先按权重策略构造与目标无关的二叉决策树,再沿目标类别的答案路径输出问题序列。

OJ: shumeng

题目 ID: CSP202312C

难度:提高+/省选-

标签:DFS 序决策树前缀和

日期: 2026-07-31 16:21

形式化题目

类别关系是一棵以 1 为根的树,每个类别有一个正权重。反复执行:在当前候选类别集合中选择一个类别 xx,使 x|x 子树内候选权重和 - 其余候选权重和| 最小,编号相同取更小者;根据目标是否属于 xx 的子树,保留对应部分。直到只剩一个类别。对 mm 个给定目标类别,输出整个提问序列。

思路

先看每个状态重新枚举候选子树节点的朴素程序,它在 nn 较大时较慢,但逻辑直接:

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-31 16:21
 * update_at: 2026-08-17 22:40
 */
// brute.cpp:小数据暴力解,每个状态重新枚举候选子树中的所有节点。
#include <bits/stdc++.h>
using namespace std;

struct DecisionNode {
    int question, yes_child, no_child;
};

int n, m, timer_count;
vector<long long> weight;
vector<int> tin, tout;
vector<vector<int> > child;
vector<DecisionNode> decision;

void dfs(int node) {
    tin[node] = ++timer_count;
    for (int i = 0; i < (int)child[node].size(); i++) dfs(child[node][i]);
    tout[node] = timer_count;
}

bool in_subtree(int node, int root) {
    return tin[root] <= tin[node] && tin[node] <= tout[root];
}

int build_decision_tree(const vector<char> &active) {
    int active_count = 0;
    for (int i = 1; i <= n; i++) active_count += active[i];
    int current = decision.size();
    decision.push_back({0, -1, -1});
    if (active_count == 1) return current;

    long long total = 0;
    for (int i = 1; i <= n; i++) {
        if (active[i]) total += weight[i];
    }
    long long best_difference = LLONG_MAX;
    int best_question = 0;
    for (int i = 1; i <= n; i++) {
        if (!active[i]) continue;
        long long inside_weight = 0;
        for (int j = 1; j <= n; j++) {
            if (active[j] && in_subtree(j, i)) inside_weight += weight[j];
        }
        long long difference = llabs(total - 2 * inside_weight);
        if (difference < best_difference
                || (difference == best_difference && i < best_question)) {
            best_difference = difference;
            best_question = i;
        }
    }

    vector<char> yes_active(n + 1, 0), no_active(n + 1, 0);
    for (int i = 1; i <= n; i++) {
        if (!active[i]) continue;
        if (in_subtree(i, best_question)) yes_active[i] = 1;
        else no_active[i] = 1;
    }
    decision[current].question = best_question;
    decision[current].yes_child = build_decision_tree(yes_active);
    int no_count = 0;
    for (int i = 1; i <= n; i++) no_count += no_active[i];
    if (no_count > 0) decision[current].no_child = build_decision_tree(no_active);
    return current;
}

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

    cin >> n >> m;
    weight.resize(n + 1);
    for (int i = 1; i <= n; i++) cin >> weight[i];
    child.resize(n + 1);
    for (int i = 2; i <= n; i++) {
        int parent;
        cin >> parent;
        child[parent].push_back(i);
    }
    tin.resize(n + 1);
    tout.resize(n + 1);
    dfs(1);

    vector<char> active(n + 1, 1);
    build_decision_tree(active);
    while (m--) {
        int target;
        cin >> target;
        int current = 0;
        bool first = true;
        while (decision[current].question != 0) {
            int question = decision[current].question;
            if (!first) cout << ' ';
            first = false;
            cout << question;
            if (in_subtree(target, question)) current = decision[current].yes_child;
            else current = decision[current].no_child;
        }
        cout << '\n';
    }

    return 0;
}

所有目标共享同一棵决策树

某个状态中下一次选择哪个类别,只由当前候选集合和权重决定,与目标类别无关。一次提问把候选集合划分为“在问题类别子树内”和“不在子树内”两部分。

因此可以从全集开始构造一棵二叉树:节点保存问题编号和两个答案分支。候选集合只会被不断划分,直到每个叶子对应一个类别。构造完成后,每个测试目标只需沿这棵树走一条路径,无需为每个目标重新模拟。

用 DFS 序计算子树权重

对原树做 DFS,得到 tintout。节点 xx 的子树对应 DFS 序上的连续区间 [tinx,toutx][tin_x,tout_x]

对当前候选集合,把候选节点的权重放在 DFS 序对应位置(非候选为 0),做一次前缀和,就能在 O(1)O(1) 求出任意类别子树内的候选权重。扫描所有候选类别即可找到最小差值;差值相同时按编号更小者更新。

复杂度来源

构造一个状态时复制并划分候选集合,决策状态总数不超过 2n12n-1 个,总复杂度为 O(n2)O(n^2),在 n2000n\le 2000 时可接受。

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-31 16:21
 * update_at: 2026-08-17 22:40
 */
#include <bits/stdc++.h>
using namespace std;

// 决策树节点:question 是要问的类别,yes/no_child 为两个答案分支的节点下标
struct DecisionNode {
    int question;
    int yes_child;
    int no_child;
};

int n, m;
int timer_count;              // DFS 序计数器
vector<long long> weight;     // 每个类别的权重
vector<int> tin, tout;        // 每个节点的 DFS 进入/离开时间戳
vector<vector<int> > child;   // 类别树的子节点列表
vector<DecisionNode> decision; // 决策树

// 对类别树做 DFS,得到每个节点的子树区间 [tin, tout]
void dfs(int node) {
    tin[node] = ++timer_count;
    for (int i = 0; i < (int)child[node].size(); i++) {
        dfs(child[node][i]);
    }
    tout[node] = timer_count;
}

// 判断 node 是否在 root 的子树内(用 DFS 序区间判断)
bool in_subtree(int node, int root) {
    return tin[root] <= tin[node] && tin[node] <= tout[root];
}

// 递归构造决策树:active[i] 表示类别 i 是否还在当前候选集合中。
// 返回新建节点的下标。
int build_decision_tree(const vector<char> &active) {
    int active_count = 0;
    for (int i = 1; i <= n; i++) active_count += active[i];

    int current = decision.size();
    decision.push_back({0, -1, -1});
    if (active_count == 1) return current; // 只剩一个类别,叶子节点

    // 用 DFS 序前缀和计算每个类别子树内的候选权重
    vector<long long> prefix(n + 1, 0);
    for (int i = 1; i <= n; i++) {
        prefix[tin[i]] = active[i] ? weight[i] : 0;
    }
    for (int i = 1; i <= n; i++) prefix[i] += prefix[i - 1];

    // 选出使 |子树权重 - 其余权重| 最小、编号最小的候选类别
    long long total = prefix[n];
    long long best_difference = LLONG_MAX;
    int best_question = 0;
    for (int i = 1; i <= n; i++) {
        if (!active[i]) continue;
        long long inside_weight = prefix[tout[i]] - prefix[tin[i] - 1];
        long long difference = llabs(total - 2 * inside_weight);
        if (difference < best_difference
                || (difference == best_difference && i < best_question)) {
            best_difference = difference;
            best_question = i;
        }
    }

    // 按目标是否在该子树中,把候选集合一分为二
    vector<char> yes_active(n + 1, 0), no_active(n + 1, 0);
    for (int i = 1; i <= n; i++) {
        if (!active[i]) continue;
        if (in_subtree(i, best_question)) yes_active[i] = 1;
        else no_active[i] = 1;
    }

    decision[current].question = best_question;
    decision[current].yes_child = build_decision_tree(yes_active);
    int no_count = 0;
    for (int i = 1; i <= n; i++) no_count += no_active[i];
    if (no_count > 0) decision[current].no_child = build_decision_tree(no_active);
    return current;
}

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

    cin >> n >> m;
    weight.resize(n + 1);
    for (int i = 1; i <= n; i++) cin >> weight[i];

    child.resize(n + 1);
    for (int i = 2; i <= n; i++) {
        int parent;
        cin >> parent;
        child[parent].push_back(i);
    }
    tin.resize(n + 1);
    tout.resize(n + 1);
    dfs(1);

    // 从全集开始一次性构造与目标无关的决策树
    vector<char> active(n + 1, 1);
    decision.reserve(2 * n);
    build_decision_tree(active);

    // 每个测试目标只需沿决策树走一条路径
    while (m--) {
        int target;
        cin >> target;
        int current = 0;
        bool first = true;
        while (decision[current].question != 0) {
            int question = decision[current].question;
            if (!first) cout << ' ';
            first = false;
            cout << question;
            // 目标在子树内走 yes 分支,否则走 no 分支
            if (in_subtree(target, question)) current = decision[current].yes_child;
            else current = decision[current].no_child;
        }
        cout << '\n';
    }

    return 0;
}

复杂度

决策树最多有 2n12n-1 个状态,每个状态用 O(n)O(n) 时间做前缀和、选择问题和划分集合,构造时间复杂度为 O(n2)O(n^2)。递归构造中的候选集合最坏为 O(n2)O(n^2) 空间,决策树与树上辅助数组为 O(n)O(n),在 n2000n\le 2000 时满足空间限制。每个测试类别输出路径的时间为其问题数。

总结

不要为每个目标类别独立模拟。把“策略如何提问”看成一棵与目标无关的决策树,先一次性构造,再对每个目标读取对应路径;DFS 序前缀和则把每个状态中的子树权重计算降为线性扫描。