树形动态规划(Tree DP)是一种在树形结构上进行动态规划的方法。在解决树形结构上的问题时,通常使用树形动态规划来优化算法的时间复杂度。
树形动态规划的核心思想是通过递归地处理树的节点,利用节点之间的关系来计算每个节点的值,并最终求解整棵树的问题。在进行树形动态规划时,可以根据具体问题的特点选择合适的遍历顺序和计算策略。
常见的树形动态规划问题包括但不限于:
- 求解树的直径(树上最远的两个节点之间的距离)
- 求解树的最长路径
- 求解树的最大独立集(在树中选择一些节点,使得任意两个节点之间没有边)
- 求解树的最小覆盖集(最少的点集,使得覆盖所有边)
- 求解树的最大权独立集(在树中选择一些节点,使得节点权值之和最大且任意两个节点之间没有边)
树形动态规划通常使用深度优先搜索(DFS)或广度优先搜索(BFS)遍历树的节点,并结合递归或迭代的方式,通过节点之间的关系逐步计算值。通过合理地设计状态转移方程和边界条件,可以高效地解决各种树形结构上的问题。
我们今天所讲的是自顶向下的树形DP,自顶向下的树形动态规划是从树的根节点开始递归计算,通过递归地处理子节点的结果来计算父节点的值。在计算过程中,可以使用记忆化搜索(Memorization)或动态规划数组来避免重复计算。
自顶向下的树形动态规划适用于解决一系列树形问题,包括但不限于以下几种类型的问题:
-
最大/最小路径和问题: 例如在一个树中找到一条从根节点到叶子节点的最大/最小路径和。
-
最大独立集问题: 在树中选取一些节点,使得这些节点互不相邻且权值和最大。
-
最小覆盖集问题: 选取最少的节点,使得所有边都至少与一个节点相连。
-
最长路径问题: 找到树中最长路径的长度或路径本身。
-
最大权重二分图匹配问题: 在树中的子节点中寻找最大权重的匹配。
-
树形背包问题: 在树中每个节点有一个权重和容量限制,选择一个子集使得总权重最大。
-
树上的动态规划问题: 如计算树中特定属性的值,例如节点之间的最大距离、特定路径的权重等。
以下是大致算法模板:
#include <iostream>
#include <vector>
const int MOD = 1e9 + 7;
const int MAXN = 100005;
std::vector<int> adj[MAXN];
std::vector<int> dp(MAXN, 0);
int dfs(int u) {
if (dp[u] != -1) {
return dp[u]; // 如果已经计算过,直接返回结果
}
int result = 0;
for (int v : adj[u]) {
result += dfs(v);
result %= MOD;
}
return dp[u] = result; // 将计算结果存储起来并返回
}
int main() {
int n;
std::cin >> n;
// 初始化邻接表和dp数组
for (int i = 0; i < n; ++i) {
adj[i].clear();
dp[i] = -1; // -1 表示还没有计算过
}
// 读入树的边信息
for (int i = 0; i < n - 1; ++i) {
int u, v;
std::cin >> u >> v;
adj[u].push_back(v);
}
// 从根节点开始递归计算
int result = dfs(0);
std::cout << result << std::endl;
return 0;
}
为了更好地了解自顶向下的树形DP,我们通过一道例题来加深印象,
题目:
蓝桥公司一共有 n 名员工,编号分别为 1∼n。
他们之间的关系就像一棵以董事长为根的树,父节点就是子节点的直接上司。
每个员工有一个快乐指数 ai。
现蓝桥董事会决定举办一场蓝桥舞会来让员工们在工作之余享受美好时光,不过对于每个员工,他们都不愿意与自己的直接上司一起参会。
董事会希望舞会的所有参会员工的快乐指数总和最大,请你求出这个最大值。
解题思路:
这个题目是一个典型的树形DP,并且可以作为一个模板题去拓展,具体步骤为:
-
定义状态:
- 使用
dp[i][0]
表示不选择节点i
时的最大值 - 使用
dp[i][1]
表示选择节点i
时的最大值 happy[i]
表示节点i
的快乐度
- 使用
-
递推关系:
- 对于节点
i
,其子节点j
的贡献可以分为两种情况:- 若选择节点
i
,则需要选择所有的子节点j
的不选情况,即dp[i][1] = ∑dp[j][0]
- 若不选择节点
i
,则可以选择子节点j
的最大值情况,即dp[i][0] = ∑max(dp[j][0], dp[j][1])
- 若选择节点
- 对于节点
-
递归计算:
- 使用深度优先搜索进行递归计算每个节点的最大值
- 遍历每个节点,以根节点开始进行递归遍历,根据子节点的情况更新父节点的状态
-
找到根节点:
- 根据输入的边信息构建树的邻接表
- 找到根节点,即没有作为其他节点的子节点的节点作为根节点
-
求解结果:
- 最终输出根节点的动态规划结果中的最大值,即为整棵树的最优解。