NeFut Logo NeFut
EN 管理员登录

树形动态规划:无上司的舞会与最大独立集问题解析

发布于:2026-05-27 09:17 最后更新:2026-06-06 13:04
#C++ #Tutorial

核心逻辑与数学原理

树形动态规划(Tree DP)是一种将动态规划的阶段推进建立在树状拓扑结构(有向无环图)上的算法模型。其核心本质在于利用后序遍历(DFS)的天然拓扑序,实现状态从子节点向父节点的线性能量传导

1. 拓扑序的几何特化

在线性 DP 中,阶段沿着一维轴(如下标 $i$)有序推进。而在树形结构中,一个父节点 $u$ 的最优决策依赖于其所有子节点 $v \\in ext{son}(u)$ 固化后的状态。由于树没有环,我们可以通过一次深度优先搜索(DFS),在递归回溯(即后序遍历位置)时收集子节点信息。此时,子节点的生命周期已经结束,其状态已是最优解,完美满足无后序性

2. 没有上司的舞会(经典最大独立集模型)

给定一棵建立有父子关系的树,每个节点都有一个快乐值。现需选出一个节点子集,使得任意有边相连的两个节点(即父子关系)不能同时被选中。求该子集的最大快乐值之和。


状态设计与算法推导

1. 状态空间定义

设 $f[u][0]$ 表示在以 $u$ 为根的子树中,不选择节点 $u$ 时所能获得的最大快乐值。 设 $f[u][1]$ 表示在以 $u$ 为根的子树中,选择节点 $u$ 时所能获得的最大快乐值。

2. 状态转移方程推导

对于节点 $u$,设其子节点集合为 $ ext{son}(u)$,当前节点的原始快乐值为 $R[u]$:

$$f[u][1] = R[u] + \sum_{v \\in \text{son}(u)} f[v][0]$$

$$f[u][0] = \sum_{v \\in \text{son}(u)} \max(f[v][0], f[v][1])$$

3. 边界条件

当 $u$ 为叶子节点($ ext{son}(u) = \emptyset$)时:

$$f[u][0] = 0, \quad f[u][1] = R[u]$$

该边界在 DFS 回溯触底时自然激活。


C++ 标准源码(NOIP风格)

以下源码为“没有上司的舞会”考场标准实现。采用高性能邻接表(std::vector)建树,严格兼容 Linux g++ -O2 编译规范。

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

const int MAXN = 6005;

int r[MAXN];          // 存储每个节点的快乐值
int f[MAXN][2];       // f[u][0]: 不选u, f[u][1]: 选u
bool has_parent[MAXN];// 标记是否有父节点,用于寻找整棵树的根节点
vector<int> tree[MAXN];// 邻接表建树

// 树形 DP 核心:利用 DFS 回溯实现自底向上的状态转移
void dfs(int u) {
    // 1. 初始化叶子节点的基本元状态
    f[u][0] = 0;
    f[u][1] = r[u];

    // 2. 纵向向下拓扑推进
    for (int v : tree[u]) {
        dfs(v); // 先让子节点去计算它们自己的子树状态

        // 3. 回溯时利用子节点状态更新父节点(自底向上贡献)
        f[u][1] += f[v][0];                       // 选了 u,子节点 v 强制不能选
        f[u][0] += max(f[v][0], f[v][1]);         // 不选 u,子节点 v 怎么大怎么来
    }
}

int main() {
    // 优化 I/O 性能
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int n;
    if (!(cin >> n)) return 0;

    for (int i = 1; i <= n; ++i) {
        cin >> r[i];
    }

    // 读入关系,建立有向树(从父节点指向子节点)
    for (int i = 1; i < n; ++i) {
        int l, k; // l 是 k 的直接下属(l 是子节点,k 是父节点)
        cin >> l >> k;
        tree[k].push_back(l);
        has_parent[l] = true; // 标记 l 有父亲
    }

    // 寻找整棵树的几何根节点(没有父亲的节点就是根)
    int root = 1;
    while (has_parent[root]) {
        root++;
    }

    // 从根节点启动 DFS 状态网络交织
    dfs(root);

    // 全局最优解必然在“选根”与“不选根”两种终态中产生
    cout << max(f[root][0], f[root][1]) << "\n";

    return 0;
}

NOIP 实战避坑指南

1. 无向图建树未跳过父节点导致死循环爆栈

2. 根节点定位错误


经典 NOIP/洛谷 真题

1. 洛谷 P1352 没有上司的舞会

2. 洛谷 P2015 二叉苹果树

$$f[u][j] = \max_{v \\in \text{son}(u)} \{ f[u][j - k] + f[v][k - 1] + \text{apple}(u, v) \}$$

该模型完美将树形结构与动态规划的容量背包融合在一起。

原文链接: local://16.1

[h] 返回首页