先按权重策略构造与目标无关的二叉决策树,再沿目标类别的答案路径输出问题序列。
OJ: shumeng
题目 ID: CSP202312C
难度:提高+/省选-
标签:树DFS 序决策树前缀和
日期: 2026-07-31 16:21
形式化题目
类别关系是一棵以 1 为根的树,每个类别有一个正权重。反复执行:在当前候选类别集合中选择一个类别
思路
先看每个状态重新枚举候选子树节点的朴素程序,它在
/**
* 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,得到 tin 和 tout。节点
对当前候选集合,把候选节点的权重放在 DFS 序对应位置(非候选为 0),做一次前缀和,就能在
复杂度来源
构造一个状态时复制并划分候选集合,决策状态总数不超过
代码
/**
* 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;
}复杂度
决策树最多有
总结
不要为每个目标类别独立模拟。把“策略如何提问”看成一棵与目标无关的决策树,先一次性构造,再对每个目标读取对应路径;DFS 序前缀和则把每个状态中的子树权重计算降为线性扫描。



