按 mex 基础层从大到小做树形 DP,并用长链剖分维护同深度的最优传递链。
OJ: luogu
题目 ID: P14637
难度:省选/NOI-
标签:树形结构树形DP长链剖分mex
日期: 2026-06-22 20:07
题意
给定一棵以 1 为根的有根树。需要给每个节点设置一个非负整数权值。
对节点 i,设 S_i 为它子树内所有节点权值构成的集合。树的价值为:
sum mex(S_i)要求在所有赋值方案中最大化这个价值。题目保证树高不超过 m。
思路
先看一个可以直接验证想法的朴素解:
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 10;
int n, m;
vector<int> sons[MAXN];
int value_a[MAXN];
int best_answer;
void collect_values(int u, set<int> &values) {
values.insert(value_a[u]);
for (int i = 0; i < (int)sons[u].size(); i++) {
collect_values(sons[u][i], values);
}
}
int calc_mex_for_node(int u) {
set<int> values;
collect_values(u, values);
int mex = 0;
while (values.count(mex)) {
mex++;
}
return mex;
}
void dfs_assign(int pos) {
if (pos > n) {
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += calc_mex_for_node(i);
}
best_answer = max(best_answer, sum);
return;
}
for (int x = 0; x <= n; x++) {
value_a[pos] = x;
dfs_assign(pos + 1);
}
}
void solve_case() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
sons[i].clear();
}
for (int i = 2; i <= n; i++) {
int p;
cin >> p;
sons[p].push_back(i);
}
best_answer = 0;
dfs_assign(1);
cout << best_answer << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve_case();
}
return 0;
}暴力枚举每个节点的权值,再计算每个子树的 mex。它能帮助理解题意,但复杂度是指数级。
mex 的关键是连续性:如果一个子树的 mex 至少为 x,那么这个子树里必须同时出现 0,1,...,x-1。因此最优赋值可以理解为在树上安排一些连续值,让尽可能多的祖先子树获得更大的 mex。
正解按一个基础层 UM 从大到小做 DP。固定 UM 时,可以先把每个节点的贡献看作基础值,再考虑能否沿一条向下链继续把 mex 抬高。
代码中维护这些量:
f[u]:当前UM下,子树u的最优价值;g[u][0]:当前UM下,从u开始继续传递连续值的最优值;g[u][1]:上一层UM+1的传递结果;best_same_depth[u][d]:在u的子树中,相对深度为d的最好传递链候选。
如果从节点 u 继续往某个儿子 v 传递,那么其他儿子就只贡献它们当前 f 的最优值。也就是说,每次传递只会选择一个主儿子继续往下,其余子树提供独立贡献。
为了快速合并同深度的候选,代码使用长链剖分:
- 预处理每个节点的最长向下链长度和重儿子;
- 重儿子复用父节点的深度数组;
- 轻儿子的深度信息再逐项合并到父节点数组中。
这样每一轮 UM 的 DFS 合并总量是
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 8005;
int n, m;
vector<int> sons[MAXN];
int siz[MAXN], height_len[MAXN], heavy_son[MAXN];
int f[MAXN]; // 固定当前 UM 时,f[u] 表示子树 u 的最优值
int g[MAXN][2]; // g[u][0] 是当前 UM,g[u][1] 是 UM+1
int outside_sum[MAXN];
int delta[MAXN];
int pool[MAXN];
int pool_len;
int *best_same_depth[MAXN];
int current_um;
void update_max(int &x, int y) {
if (y > x) {
x = y;
}
}
void dfs_prepare(int u) {
siz[u] = 1;
height_len[u] = 1;
heavy_son[u] = 0;
for (int i = 0; i < (int)sons[u].size(); i++) {
int v = sons[u][i];
dfs_prepare(v);
siz[u] += siz[v];
if (height_len[v] + 1 > height_len[u]) {
height_len[u] = height_len[v] + 1;
heavy_son[u] = v;
}
}
}
void dfs_dp(int u) {
int child_f_sum = 0;
if (best_same_depth[u] == nullptr) {
best_same_depth[u] = pool + pool_len;
pool_len += height_len[u];
}
if (heavy_son[u] != 0) {
best_same_depth[heavy_son[u]] = best_same_depth[u] + 1;
}
f[u] = current_um * siz[u];
g[u][0] = current_um * siz[u];
for (int i = 0; i < (int)sons[u].size(); i++) {
int v = sons[u][i];
dfs_dp(v);
child_f_sum += f[v];
}
for (int i = 0; i < (int)sons[u].size(); i++) {
int v = sons[u][i];
outside_sum[v] = child_f_sum - f[v];
update_max(g[u][0], g[v][1] + current_um + outside_sum[v]);
}
if (heavy_son[u] != 0) {
int hv = heavy_son[u];
delta[u] = delta[hv] + outside_sum[hv];
for (int i = 0; i < (int)sons[u].size(); i++) {
int v = sons[u][i];
if (v == hv) {
continue;
}
for (int d = 1; d <= height_len[v]; d++) {
int val = best_same_depth[v][d] + outside_sum[v] + delta[v] - delta[u];
update_max(best_same_depth[u][d + 1], val);
}
}
}
best_same_depth[u][1] = g[u][0] - delta[u];
if (height_len[u] >= current_um) {
int val = best_same_depth[u][current_um] + delta[u] + current_um * (current_um - 1);
update_max(f[u], val);
}
}
void solve_case() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
sons[i].clear();
}
for (int i = 2; i <= n; i++) {
int p;
cin >> p;
sons[p].push_back(i);
}
dfs_prepare(1);
for (int i = 1; i <= n; i++) {
f[i] = (m + 2) * siz[i];
g[i][0] = (m + 2) * siz[i];
g[i][1] = 0;
}
for (current_um = m + 1; current_um >= 1; current_um--) {
pool_len = 0;
for (int i = 0; i <= n + 2; i++) {
pool[i] = 0;
}
for (int i = 1; i <= n; i++) {
best_same_depth[i] = nullptr;
delta[i] = 0;
swap(g[i][0], g[i][1]);
}
dfs_dp(1);
}
cout << g[1][0] << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
solve_case();
}
return 0;
}复杂度
外层枚举 UM,最多 m+1 轮。每轮通过长链剖分完成一次
总时间复杂度为:
O(nm)空间复杂度为
总结
本题不是直接给每个节点算 mex,而是反过来考虑“要让 mex 达到某一层,需要子树里提供哪些连续值”。
固定基础层 UM 后,树形 DP 只需要决定哪条儿子链继续传递,其他子树取当前最优贡献。长链剖分负责把“同深度的最优链候选”高效合并起来。
一图流解析
这张图把本题的建模、关键转移、实现检查和训练方法压缩到一页,适合读完正文后复盘。

:::