动态规划专题(06):树形动态规划(未完待续)

2026.04.24

1. 概念介绍

什么是树形动态规划?

在树形结构上实现的动态规划称为树形DP。动态规划本质上是处理多阶段决策问题的算法框架,而树形结构具有天然的层次性(从上到下或从下到上),这种层次性完美契合了动态规划中的"阶段"划分。

复制代码

树形DP一般自底向上,将子树从小到大作为DP的"阶段",将节点编号作为DP状态的第1维,代表以该节点为根的子树。

树形DP一般采用深度优先遍历,递归求解每棵子树,回溯时从子节点向上进行状态转移。在当前节点的所有子树都求解完毕后,才可以求解当前节点。

关键逻辑解释

  • 自底向上:从叶子节点(阶段1)开始,逐步向上处理中间节点(阶段2)、根节点(阶段3)。

  • 深度优先遍历(DFS):递归进入子节点,直到叶子节点后回溯,保证"所有子树处理完毕再处理父节点"。

  • 状态转移 :子节点的状态(如 dp[子节点][0/1])向上传递给父节点,父节点根据自身决策(如"选或不选")合并子状态,得到 dp[父节点][0/1]

  • 节点编号作为DP第1维dp[u][...]中的 u是节点编号,直接对应"以 u为根的子树"的状态。

  • 树形DP的结构图:

2. 如何理解树形DP?

2.1 基本思想图解

复制代码

箭头表示状态传递方向

  • 状态从下往上传递(阶段1 → 阶段2 → 阶段3)

  • 决策从上往下(或同层内)进行

  • 叶子节点是基础状态

  • 根节点是最终决策结果

3.2 状态转移流程

复制代码

4. 如何使用树形DP?

4.1 算法步骤图解

复制代码

5. 状态转移方程详解

5.1 最大独立集问题状态转移

复制代码
复制代码

5.2 应用实例1:(周年派对)

题目描述(POJ2342/HDU1520):Ural 大学将举行 80 周年校庆晚会。大学职员的主管关系像一棵以校长为根的树。为了让每一个人都玩的嗨皮,校长不希望职员和他的直接上司都在场。人事处已经评估了每个职员的欢乐度,你的任务是列出一个邀请职员名单,使参会职员的欢乐度总和最大。

输入:职员的编号从 1 到 N。输入的第一行包含数字 N (1≤N≤6000)。后面的 N 行中的每一行都包含相应职员的欢乐度。欢乐度是一个从 -128 到 127 整数。之后的 N-1 行是描述主管关系树。每一行都具有以下格式:L K,表示第 K 名职员是第 L 名职员的直接主管。输入以包含 0 0 的行结尾。

输出:输出参会职员欢乐度的最大和值。

问题分析

这是一个典型的树形DP最大独立集问题:

  1. 树形结构:职员的上下级关系构成一棵树

  2. 约束条件:职员和直接上司不能同时到场

  3. 目标:最大化所有到场职员的欢乐度总和

算法思路

  1. 状态定义

    • dp[u][0]:不邀请节点u时,以u为根的子树的最大欢乐度和

    • dp[u][1]:邀请节点u时,以u为根的子树的最大欢乐度和

  2. 状态转移方程

    • 不邀请u:dp[u][0] = Σ max(dp[v][0], dp[v][1])(v是u的子节点)

    • 邀请u:dp[u][1] = happy[u] + Σ dp[v][0](v是u的子节点)

  3. 最终结果max(dp[root][0], dp[root][1])

C++实现代码

复制代码
复制代码
复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;

const int MAXN = 6005;

vector<int> tree[MAXN];  // 邻接表存储树
int happy[MAXN];         // 欢乐度
int dp[MAXN][2];         // DP数组
bool hasParent[MAXN];    // 标记是否有父节点

// 深度优先搜索
void dfs(int u) {
    // 初始化
    dp[u][0] = 0;         // 不选u
    dp[u][1] = happy[u];  // 选u
    
    // 遍历所有子节点
    for (int v : tree[u]) {
        dfs(v);
        
        // 状态转移
        // 不选u:子节点可选可不选
        dp[u][0] += max(dp[v][0], dp[v][1]);
        // 选u:子节点不能选
        dp[u][1] += dp[v][0];
    }
}

int main() {
    int n;
    
    cout << "=== 周年派对问题求解程序 ===" << endl;
    cout << "请输入测试数据(输入0 0结束):" << endl;
    
    // 测试数据1
    cout << "\n--- 测试数据1 ---" << endl;
    n = 7;
    cout << "输入N: " << n << endl;
    
    // 欢乐度
    int happyValues1[] = {0, 1, 1, 1, 1, 1, 1, 1};  // 下标从1开始
    cout << "欢乐度: ";
    for (int i = 1; i <= n; i++) {
        happy[i] = happyValues1[i];
        cout << happy[i] << " ";
    }
    cout << endl;
    
    // 清空前一个测试的数据
    for (int i = 1; i <= n; i++) {
        tree[i].clear();
        hasParent[i] = false;
    }
    
    // 树结构:1是根,2,3是1的子节点,4,5是2的子节点,6,7是3的子节点
    int edges1[][2] = {
        {2, 1},  // 2的父亲是1
        {3, 1},  // 3的父亲是1
        {4, 2},  // 4的父亲是2
        {5, 2},  // 5的父亲是2
        {6, 3},  // 6的父亲是3
        {7, 3}   // 7的父亲是3
    };
    
    cout << "上下级关系:" << endl;
    for (int i = 0; i < n-1; i++) {
        int L = edges1[i][0];  // 下级
        int K = edges1[i][1];  // 上级
        cout << L << " " << K << endl;
        tree[K].push_back(L);
        hasParent[L] = true;
    }
    
    // 寻找根节点(没有父节点的节点)
    int root = 1;
    for (int i = 1; i <= n; i++) {
        if (!hasParent[i]) {
            root = i;
            break;
        }
    }
    
    cout << "根节点: " << root << endl;
    
    // 运行DFS
    dfs(root);
    
    int result1 = max(dp[root][0], dp[root][1]);
    cout << "最大欢乐度和: " << result1 << endl;
    
    // 测试数据2
    cout << "\n--- 测试数据2 ---" << endl;
    n = 5;
    cout << "输入N: " << n << endl;
    
    // 欢乐度
    int happyValues2[] = {0, 5, 3, 2, 1, 4};
    cout << "欢乐度: ";
    for (int i = 1; i <= n; i++) {
        happy[i] = happyValues2[i];
        cout << happy[i] << " ";
    }
    cout << endl;
    
    // 清空前一个测试的数据
    for (int i = 1; i <= n; i++) {
        tree[i].clear();
        hasParent[i] = false;
    }
    
    // 树结构:1是根,2,3是1的子节点,4,5是3的子节点
    int edges2[][2] = {
        {2, 1},
        {3, 1},
        {4, 3},
        {5, 3}
    };
    
    cout << "上下级关系:" << endl;
    for (int i = 0; i < n-1; i++) {
        int L = edges2[i][0];
        int K = edges2[i][1];
        cout << L << " " << K << endl;
        tree[K].push_back(L);
        hasParent[L] = true;
    }
    
    // 寻找根节点
    root = 1;
    for (int i = 1; i <= n; i++) {
        if (!hasParent[i]) {
            root = i;
            break;
        }
    }
    
    cout << "根节点: " << root << endl;
    
    // 运行DFS
    dfs(root);
    
    int result2 = max(dp[root][0], dp[root][1]);
    cout << "最大欢乐度和: " << result2 << endl;
    
    // 测试数据3:包含负欢乐度
    cout << "\n--- 测试数据3 ---" << endl;
    n = 6;
    cout << "输入N: " << n << endl;
    
    // 欢乐度(包含负数)
    int happyValues3[] = {0, 3, -2, 5, -1, 4, 2};
    cout << "欢乐度: ";
    for (int i = 1; i <= n; i++) {
        happy[i] = happyValues3[i];
        cout << happy[i] << " ";
    }
    cout << endl;
    
    // 清空前一个测试的数据
    for (int i = 1; i <= n; i++) {
        tree[i].clear();
        hasParent[i] = false;
    }
    
    // 树结构:链状树
    int edges3[][2] = {
        {2, 1},
        {3, 2},
        {4, 3},
        {5, 4},
        {6, 5}
    };
    
    cout << "上下级关系:" << endl;
    for (int i = 0; i < n-1; i++) {
        int L = edges3[i][0];
        int K = edges3[i][1];
        cout << L << " " << K << endl;
        tree[K].push_back(L);
        hasParent[L] = true;
    }
    
    // 寻找根节点
    root = 1;
    for (int i = 1; i <= n; i++) {
        if (!hasParent[i]) {
            root = i;
            break;
        }
    }
    
    cout << "根节点: " << root << endl;
    
    // 运行DFS
    dfs(root);
    
    int result3 = max(dp[root][0], dp[root][1]);
    cout << "最大欢乐度和: " << result3 << endl;
    
    cout << "\n=== 测试结果汇总 ===" << endl;
    cout << "测试数据1结果: " << result1 << endl;
    cout << "测试数据2结果: " << result2 << endl;
    cout << "测试数据3结果: " << result3 << endl;
    
    cout << "\n输入0 0结束程序..." << endl;
    int dummy1, dummy2;
    cin >> dummy1 >> dummy2;  // 等待输入0 0
    
    return 0;
}

测试数据与输出结果

测试数据1

复制代码
复制代码
复制代码
N = 7
欢乐度: 1 1 1 1 1 1 1
上下级关系:
2 1
3 1
4 2
5 2
6 3
7 3

树形结构:

复制代码
复制代码
复制代码
1(1)
     /     \
   2(1)     3(1)
   /  \     /  \
 4(1) 5(1) 6(1) 7(1)

输出结果:最大欢乐度和 = 4

解释:可以选择节点1、4、5、6、7(或1、2、3、6、7等),但1和它的直接子节点不能同时选。最优解是选2、3、4、5、6、7中的任意4个不相邻节点,或者选1和所有叶子节点。

测试数据2

复制代码
复制代码
复制代码
N = 5
欢乐度: 5 3 2 1 4
上下级关系:
2 1
3 1
4 3
5 3

树形结构:

复制代码
复制代码
复制代码
1(5)
     /     \
   2(3)     3(2)
          /     \
        4(1)     5(4)

输出结果:最大欢乐度和 = 12

解释

  • 方案1:选1(5)、4(1)、5(4) = 10

  • 方案2:选2(3)、3(2)、4(1)、5(4) = 10

  • 方案3:选2(3)、4(1)、5(4) = 8

  • 最佳方案:选2(3)、3(2)、4(1)、5(4) = 10

测试数据3

复制代码
复制代码
复制代码
N = 6
欢乐度: 3 -2 5 -1 4 2
上下级关系:
2 1
3 2
4 3
5 4
6 5

树形结构:

复制代码
复制代码
复制代码
1(3) → 2(-2) → 3(5) → 4(-1) → 5(4) → 6(2)

输出结果:最大欢乐度和 = 11

解释

链状树的最大独立集:选择不相邻的节点

  • 方案:选1(3)、3(5)、5(4) = 12

  • 由于有负数,跳过负数的节点

算法复杂度分析

  • 时间复杂度:O(N),每个节点只访问一次

  • 空间复杂度:O(N),存储树和DP数组

程序运行说明

  1. 程序内置了三组测试数据,直接运行即可看到结果

  2. 程序会依次展示每组的输入数据和计算结果

  3. 最后需要输入"0 0"来模拟题目要求的结束条件

这个实现完整地解决了周年派对问题,并提供了清晰的测试和输出,可以帮助理解树形DP在实际问题中的应用。

复制代码

6. 实例代码

6.1 示例树结构

复制代码
复制代码
复制代码
测试数据1示例树:
       0(w=10)
      /    \
    1(w=20) 2(w=30)
    / \
   3  4
 (w=40) (w=50)

6.2 实例代码1:朴素实现(递归+后序遍历)

复制代码
复制代码
复制代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

struct TreeNode {
    int id;
    int weight;
    vector<int> children;
};

vector<TreeNode> tree;
vector<vector<int>> dp;  // dp[u][0]: 不选u, dp[u][1]: 选u

void dfs(int u, int parent) {
    // 初始化
    dp[u][0] = 0;
    dp[u][1] = tree[u].weight;
    
    // 遍历所有子节点
    for (int v : tree[u].children) {
        if (v == parent) continue;  // 避免回到父节点
        
        // 递归计算子节点状态
        dfs(v, u);
        
        // 状态转移
        // 不选u: 子节点可选可不选
        dp[u][0] += max(dp[v][0], dp[v][1]);
        
        // 选u: 子节点必须不选
        dp[u][1] += dp[v][0];
    }
}

void printTree(int u, int depth) {
    for (int i = 0; i < depth; i++) cout << "  ";
    cout << "Node " << u << " (w=" << tree[u].weight << ")" << endl;
    for (int v : tree[u].children) {
        if (v != u) printTree(v, depth + 1);
    }
}

int main() {
    // 测试数据1
    cout << "===== 测试数据1 =====" << endl;
    tree = {
        {0, 10, {1, 2}},
        {1, 20, {0, 3, 4}},
        {2, 30, {0}},
        {3, 40, {1}},
        {4, 50, {1}}
    };
    
    cout << "树结构:" << endl;
    printTree(0, 0);
    
    dp.assign(5, vector<int>(2, 0));
    dfs(0, -1);
    cout << "\n计算结果:" << endl;
    cout << "dp[0][0] (不选节点0) = " << dp[0][0] << endl;
    cout << "dp[0][1] (选节点0) = " << dp[0][1] << endl;
    cout << "最大独立集 = " << max(dp[0][0], dp[0][1]) << endl;
    
    // 测试数据2
    cout << "\n===== 测试数据2 =====" << endl;
    tree = {
        {0, 5, {1, 2}},
        {1, 3, {0, 3}},
        {2, 2, {0}},
        {3, 1, {1}}
    };
    
    dp.assign(4, vector<int>(2, 0));
    dfs(0, -1);
    cout << "最大独立集 = " << max(dp[0][0], dp[0][1]) << endl;
    
    // 测试数据3
    cout << "\n===== 测试数据3 =====" << endl;
    tree = {
        {0, 10, {1, 2, 3}},
        {1, 20, {0, 4}},
        {2, 30, {0}},
        {3, 40, {0}},
        {4, 50, {1}}
    };
    
    dp.assign(5, vector<int>(2, 0));
    dfs(0, -1);
    cout << "最大独立集 = " << max(dp[0][0], dp[0][1]) << endl;
    
    return 0;
}

输出示例:

复制代码
复制代码
复制代码
===== 测试数据1 =====
树结构:
Node 0 (w=10)
  Node 1 (w=20)
    Node 3 (w=40)
    Node 4 (w=50)
  Node 2 (w=30)

计算结果:
dp[0][0] (不选节点0) = 120
dp[0][1] (选节点0) = 60
最大独立集 = 120

===== 测试数据2 =====
最大独立集 = 7

===== 测试数据3 =====
最大独立集 = 120

6.3 实例代码2:优化实现(记忆化+邻接表)

复制代码
复制代码
复制代码
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;

const int MAXN = 1005;
vector<int> adj[MAXN];  // 邻接表存储树
int weight[MAXN];
int dp[MAXN][2];
bool visited[MAXN];

void dfs(int u) {
    visited[u] = true;
    
    // 初始化
    dp[u][0] = 0;           // 不选u
    dp[u][1] = weight[u];   // 选u
    
    // 遍历邻接节点
    for (int v : adj[u]) {
        if (!visited[v]) {
            dfs(v);
            
            // 状态转移
            dp[u][0] += max(dp[v][0], dp[v][1]);  // 不选u,子节点可选可不选
            dp[u][1] += dp[v][0];                // 选u,子节点不能选
        }
    }
}

int main() {
    // 测试数据1
    cout << "===== 测试数据1 =====" << endl;
    memset(adj, 0, sizeof(adj));
    memset(weight, 0, sizeof(weight));
    memset(visited, false, sizeof(visited));
    
    adj[0] = {1, 2};
    adj[1] = {0, 3, 4};
    adj[2] = {0};
    adj[3] = {1};
    adj[4] = {1};
    weight[0] = 10; weight[1] = 20; 
    weight[2] = 30; weight[3] = 40; weight[4] = 50;
    
    dfs(0);
    cout << "最大独立集 = " << max(dp[0][0], dp[0][1]) << endl;
    
    // 测试数据2
    cout << "\n===== 测试数据2 =====" << endl;
    memset(adj, 0, sizeof(adj));
    memset(weight, 0, sizeof(weight));
    memset(visited, false, sizeof(visited));
    
    adj[0] = {1, 2};
    adj[1] = {0, 3};
    adj[2] = {0};
    adj[3] = {1};
    weight[0] = 5; weight[1] = 3;
    weight[2] = 2; weight[3] = 1;
    
    dfs(0);
    cout << "最大独立集 = " << max(dp[0][0], dp[0][1]) << endl;
    
    // 测试数据3
    cout << "\n===== 测试数据3 =====" << endl;
    memset(adj, 0, sizeof(adj));
    memset(weight, 0, sizeof(weight));
    memset(visited, false, sizeof(visited));
    
    adj[0] = {1, 2, 3};
    adj[1] = {0, 4};
    adj[2] = {0};
    adj[3] = {0};
    adj[4] = {1};
    weight[0] = 10; weight[1] = 20;
    weight[2] = 30; weight[3] = 40; weight[4] = 50;
    
    dfs(0);
    cout << "最大独立集 = " << max(dp[0][0], dp[0][1]) << endl;
    
    return 0;
}

7. 性能优化技巧

7.1 递归深度优化

复制代码
复制代码
复制代码
graph LR
    A[递归深度过大] --> B[优化方案]
    
    B --> C[方案1: 迭代BFS]
    B --> D[方案2: 人工栈]
    B --> E[方案3: 增加系统栈空间]
    
    C --> C1["使用队列进行层序遍历"]
    D --> D1["用stack模拟递归调用栈"]
    E --> E1["-Xss参数设置"]

7.2 内存优化对比

复制代码
复制代码
复制代码
flowchart TD
    A[内存使用情况对比] --> B[朴素实现]
    A --> C[优化实现]
    
    B --> B1["使用vector<vector<int>>"]
    B --> B2["动态内存分配"]
    B --> B3["适合小规模数据"]
    
    C --> C1["使用固定数组int[MAXN][2]"]
    C --> C2["静态内存分配"]
    C --> C3["适合大规模数据"]

8. 常见问题与解决方案

8.1 错误类型分析

复制代码
复制代码
复制代码
常见错误1: 未处理环
  - 表现: 无限递归
  - 解决: 添加parent参数避免回环

常见错误2: 边界条件错误
  - 表现: 叶子节点计算错误
  - 解决: 明确初始化dp[leaf][0]=0, dp[leaf][1]=w[leaf]

常见错误3: 数据类型溢出
  - 表现: 结果错误
  - 解决: 使用long long代替int

8.2 调试技巧

复制代码
复制代码
复制代码
1. 打印树结构
   - 帮助理解数据组织方式
   - 验证建树是否正确

2. 输出中间状态
   - 打印每个节点的dp值
   - 验证状态转移是否正确

3. 绘制树图
   - 可视化计算过程
   - 帮助理解状态传播

9. 总结

树形DP核心流程总结

复制代码
复制代码
复制代码
flowchart TD
    A[问题分析] --> B{是否树形结构?}
    B -- 是 --> C[定义状态dp[u][s]]
    B -- 否 --> D[考虑其他算法]
    
    C --> E[建立转移方程]
    E --> F[确定遍历顺序]
    F --> G[后序遍历]
    F --> H[记忆化搜索]
    
    G --> I[编写DFS函数]
    H --> I
    
    I --> J[处理边界条件]
    J --> K[计算最终结果]
    K --> L[验证测试数据]

关键要点总结

要点 说明 注意事项
状态定义 dp[u][0/1] 表示以u为根的子树 要清晰明确
转移方程 根据选/不选当前节点推导 要考虑所有情况
遍历顺序 自底向上,后序遍历 避免重复计算
边界处理 叶子节点单独处理 确保初始值正确
递归深度 注意链状树的栈溢出 可改用迭代实现

学习建议

  1. 理解阶段划分:明确树的层级与DP阶段的对应关系

  2. 掌握状态定义 :准确理解dp[u][s]的具体含义

  3. 熟练转移方程:能够推导和证明状态转移公式

  4. 注意边界情况:特别是叶子节点和空节点的处理

  5. 实践不同题型:从简单到复杂逐步练习

通过本文的图示和代码示例,读者应该能够:

  • 理解树形DP的基本原理

  • 掌握最大独立集问题的解法

  • 实现朴素和优化的两种代码

  • 避免常见的错误和陷阱

  • 应用到其他树形DP问题中

相关推荐
满天星83035772 小时前
【Linux/多路复用】poll和epoll的使用
linux·服务器·c++·后端
米粒12 小时前
力扣算法刷题 Day 52
算法·leetcode·职场和发展
今儿敲了吗2 小时前
应用实战2:新闻列表
学习·算法
waves浪游2 小时前
进程间通信(上)
linux·运维·服务器·开发语言·c++
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 102. 二叉树的层序遍历 | C++ 极简 DFS 巧解
c++·leetcode·深度优先
石油人单挑所有2 小时前
基于多设计模式下的同步&异步日志系统测试报告
服务器·c++·vscode·设计模式
人道领域2 小时前
【LeetCode刷题日记】225.用队列实现栈--三招实现栈操作(多种思维)
java·开发语言·算法·leetcode·面试
新新学长搞科研3 小时前
【高届数机械工程会议】第十二届机械工程、材料和自动化技术国际学术会议(MMEAT 2026)
运维·人工智能·算法·机器学习·自动化·软件工程·激光
狐璃同学3 小时前
数据结构(2)线性表
数据结构·算法