树上最大权独立集模板题:通过树形 DP 记录当前节点选或不选两种状态,自底向上合并子树最优解。
OJ: luogu
题目 ID: P1352
难度:普及
标签:树形DP动态规划树
创建: 2026-06-22 22:59
更新: 2026-10-23 23:58
后置题目
luogu P4084[USACO17DEC] Barn Painting GB 的 dp[u][c] 直接沿用 A 教的「父节点状态禁止孩子取同一状态」这一步,只是把 0/1 两态扩成 3 色、并把取 max 换成同色排除后求和再连乘。luogu U661996疯狂的背包问题(14) - 有依赖的背包问题B 的 dp[u][j] 定义直接沿用 A 教的「父节点是否被选决定子节点能否被选」这一状态刻画(A 用 dp[u][0/1] 表达「父亲来了,孩子不能来」,B 硬编码「u 必须被选,不选则整棵子树都不能选」)并沿用其 DFS 自底向上合并子树的骨架;B 新增的只是容量维与给孩子分配容量的分组背包合并。
形式化题目
给定一棵包含
从树中选取一个结点子集,满足任意一条树边连接的两个结点不能被同时选中(即选出的点互不相邻,构成独立集)。求被选结点的权值之和的最大值。
暴力解法
思路
每个结点只有“选入集合”与“不选入集合”两种决策。
可以通过递归 DFS 枚举所有
代码
cpp
#include <bits/stdc++.h>
using namespace std;
// brute.cpp:小数据枚举每个员工是否参加,检查上下级冲突。
const int MAXN = 25;
int n;
int happy[MAXN];
int child_node[MAXN], parent_node[MAXN];
int choose_flag[MAXN];
int answer;
void dfs_enum(int pos) {
if (pos == n + 1) {
for (int i = 1; i < n; i++) {
if (choose_flag[child_node[i]] && choose_flag[parent_node[i]]) {
return;
}
}
int sum = 0;
for (int i = 1; i <= n; i++) {
if (choose_flag[i]) {
sum += happy[i];
}
}
answer = max(answer, sum);
return;
}
choose_flag[pos] = 0;
dfs_enum(pos + 1);
choose_flag[pos] = 1;
dfs_enum(pos + 1);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> happy[i];
}
for (int i = 1; i < n; i++) {
cin >> child_node[i] >> parent_node[i];
}
answer = 0;
dfs_enum(1);
cout << answer << '\n';
return 0;
}复杂度
- 时间复杂度:枚举
个结点的状态需要 次递归,对每种方案校验边并求和需要 ,总时间复杂度为 。 - 空间复杂度:递归深度与状态标记数组占用
空间。
瓶颈
方案枚举是全局的盲目搜索。实际上,结点
正解
思路
本题是树上最大权独立集的经典模型。
考虑树的无后向性:在以
- 若不选
:每个直接孩子 既可以选,也可以不选,各孩子子树之间相互独立,贪心选取各自的最优方案。 - 若选择
:根据题意,其所有直接孩子 都不能被选择,每个孩子子树只能取不选 的最优方案。
状态设计
定义:
表示不选结点 时,以 为根的子树所能获得的最大权值和。 表示选择结点 时,以 为根的子树所能获得的最大权值和。
状态转移方程
自底向上遍历每个结点
- 基础值初始化:
, 。 - 遍历直接孩子
:
求解流程
- 读入数据建有向树(由父结点连向子结点),通过入度数组标记有父结点的点,未被标记的即为整棵树的根结点
。 - 从
出发进行一次后序深度优先搜索(DFS),先递归求解所有孩子子树的 DP 值,再聚合得到当前结点的 DP 值。 - 最终答案即为根结点选或不选两种情况的最大值:
。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 6005;
int n;
int happy[MAXN];
vector<int> children[MAXN];
bool has_parent[MAXN];
int dp[MAXN][2]; // dp[u][0]:不选 u 的最大快乐值;dp[u][1]:选择 u 的最大快乐值。
void read_input() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> happy[i];
}
for (int i = 1; i < n; i++) {
int child, parent;
cin >> child >> parent;
children[parent].push_back(child);
has_parent[child] = true;
}
}
void dfs(int u) {
dp[u][0] = 0;
dp[u][1] = happy[u];
for (int i = 0; i < (int)children[u].size(); i++) {
int v = children[u][i];
dfs(v);
// 不选 u 时,孩子 v 可选可不选,取更优。
dp[u][0] += max(dp[v][0], dp[v][1]);
// 选择 u 时,直接下属 v 不能参加。
dp[u][1] += dp[v][0];
}
}
void solve() {
int root = 1;
for (int i = 1; i <= n; i++) {
if (!has_parent[i]) {
root = i;
break;
}
}
dfs(root);
cout << max(dp[root][0], dp[root][1]) << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
return 0;
}复杂度
- 时间复杂度:DFS 遍历整棵树,每个结点和每条边各访问一次,单次状态转移为
,总时间复杂度为 。在 规模下可毫秒级完成。 - 空间复杂度:邻接表存树需要
空间,DP 数组和递归调用栈各占 空间,总空间复杂度为 。
总结
树形 DP 的核心抓手在于分析“父结点与子结点的状态约束关系”。
在本题中,约束关系极其明确:选了父亲就绝对不能选孩子;不选父亲则孩子选不选均可。因此仅需用