news 2026/4/25 0:48:33

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
动态规划专题(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问题中

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/25 0:37:32

特征选择子空间集成:高维数据建模的实践指南

1. 特征选择子空间集成方法概述在机器学习实践中&#xff0c;高维数据带来的"维度灾难"一直是困扰模型性能的关键问题。我十年前第一次处理基因表达数据集时&#xff0c;面对上万个特征但仅有几百个样本的情况&#xff0c;传统机器学习方法几乎全部失效。正是从那时起…

作者头像 李华
网站建设 2026/4/25 0:32:31

2026届最火的降AI率方案推荐

Ai论文网站排名&#xff08;开题报告、文献综述、降aigc率、降重综合对比&#xff09; TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 想让文本被 AIGC 检测到的概率降低&#xff0c;可以从下面几个层面来开始着手。其中一个是&…

作者头像 李华