用树形 DP 计算必须保留每个点时的最大连通块权值,负贡献子树直接剪掉。
OJ: luogu
题目 ID: P1122
难度:普及
标签:树形DP动态规划树
创建: 2026-06-22 23:07
更新: 2026-10-26 14:17
形式化题目
给定一棵包含
通过删除若干条边并保留其中一个连通分量,相当于在树上选择一个非空连通点子集
求所有合法非空连通块中点权和的最大值。
暴力解法
思路
对于较小的
对于每一个子集:
- 选取其中任意一个点作为起点进行广度优先搜索(BFS),只在子集
内部的相邻点之间拓展; - 检查子集中的所有点是否全部被访问到。若是,则该子集在原树上构成连通块;
- 计算该连通块的点权和,并更新全局最大值。
代码
#include <bits/stdc++.h>
using namespace std;
// brute.cpp:枚举小数据所有点集,检查是否连通。
const int MAXN = 22;
int n;
int beauty[MAXN];
bool edge_exists[MAXN][MAXN];
bool connected_subset(int mask) {
int start = -1;
for (int i = 0; i < n; i++) {
if ((mask & (1 << i)) != 0) {
start = i;
break;
}
}
if (start == -1) {
return false;
}
queue<int> que;
bool visited[MAXN] = {false};
visited[start] = true;
que.push(start);
while (!que.empty()) {
int u = que.front();
que.pop();
for (int v = 0; v < n; v++) {
if ((mask & (1 << v)) != 0 && edge_exists[u + 1][v + 1] && !visited[v]) {
visited[v] = true;
que.push(v);
}
}
}
for (int i = 0; i < n; i++) {
if ((mask & (1 << i)) != 0 && !visited[i]) {
return false;
}
}
return true;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> beauty[i];
}
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
edge_exists[u][v] = true;
edge_exists[v][u] = true;
}
int answer = -1000000000;
for (int mask = 1; mask < (1 << n); mask++) {
if (!connected_subset(mask)) {
continue;
}
int sum = 0;
for (int i = 0; i < n; i++) {
if ((mask & (1 << i)) != 0) {
sum += beauty[i + 1];
}
}
answer = max(answer, sum);
}
cout << answer << '\n';
return 0;
}复杂度
- 时间复杂度:共有
个非空子集,每次通过 BFS 判定连通性需要遍历点与边,时间复杂度为 。 - 空间复杂度:存储邻接关系与访问标记数组,空间复杂度为
。
瓶颈
当
正解
思路
选定任意一个结点为根(例如
任何一个非空连通块
据此设计树形动态规划状态:
定义
考虑结点
- 各个子树之间仅通过
相连,它们在子树内的选择决策互相独立; - 若以子结点
为顶端的最大连通块贡献 ,则将该部分连通块接在 上能使得总权值增大; - 若
,接上该子树只会使点权和减少或不变,因此修剪掉与 相连的边更为优异。
状态转移方程为:
由于任意合法的非空连通块都会在其最浅结点
转移演算: 样例树上的逐层结果
以样例
flowchart TB N1["1<br/>a=-1"] --> N4["4<br/>a=1"] N4 --> N7["7<br/>a=0"] N7 --> N5["5<br/>a=1"] N7 --> N6["6<br/>a=1"] N5 --> N2["2<br/>a=-1"] N6 --> N3["3<br/>a=-1"]
后序遍历顺序是
| 处理顺序 | 结点 | 各儿子 |
保留的子树 | ||
|---|---|---|---|---|---|
| 1 | 2 | 无(叶子) | 无 | ||
| 2 | 5 | 剪掉 |
|||
| 3 | 3 | 无(叶子) | 无 | ||
| 4 | 6 | 剪掉 |
|||
| 5 | 7 | 都保留 | |||
| 6 | 4 | 保留 |
|||
| 7 | 1 | 保留 |
读表时注意第 2、4 行: 儿子
最终
为什么可以直接取
下表对比"最优连通块由哪个结点当最浅点", 说明
| 连通块 | 最浅结点 | 被哪个 |
权值 |
|---|---|---|---|
每个连通块恰好有一个最浅结点, 因此恰好被一个
代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 16005;
int n;
int beauty[MAXN];
vector<int> graph_edges[MAXN];
int dp[MAXN]; // dp[u] 表示必须保留 u,且只在 u 子树中选择连通块时的最大美丽和。
int answer;
void read_input() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> beauty[i];
}
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
graph_edges[u].push_back(v);
graph_edges[v].push_back(u);
}
}
void dfs(int u, int parent) {
dp[u] = beauty[u];
for (int i = 0; i < (int)graph_edges[u].size(); i++) {
int v = graph_edges[u][i];
if (v == parent) {
continue;
}
dfs(v, u);
// 子树贡献为正时才接到 u 上;负贡献剪掉更优。
if (dp[v] > 0) {
dp[u] += dp[v];
}
}
answer = max(answer, dp[u]);
}
void solve() {
answer = beauty[1];
dfs(1, 0);
cout << answer << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
return 0;
}复杂度
- 时间复杂度:树上后序遍历(DFS)每个结点和每条边恰好访问常数次,时间复杂度为
。 - 空间复杂度:使用邻接表存图以及递归调用栈,空间复杂度为
。
总结
树上连通块最值问题的关键切入点是确定连通块在树上的最高结点(浅层根)。一旦最高点确定,向各个子结点的拓展决策彼此解耦,每个子树只需要判断贡献是否大于零,正贡献保留、负贡献剪除即可。