目录
[第 1 行:long sum = val[u];](#第 1 行:long sum = val[u];)
[第 2--3 行:遍历邻居 + 跳过父节点](#第 2–3 行:遍历邻居 + 跳过父节点)
[第 4 行:long childSum = dfs(v, u, opp, val);](#第 4 行:long childSum = dfs(v, u, opp, val);)
[第 5 行:if (childSum > 0) sum += childSum;](#第 5 行:if (childSum > 0) sum += childSum;)
[第 6 行:if (sum > maxsum) maxsum = sum;](#第 6 行:if (sum > maxsum) maxsum = sum;)
[第 7 行:return sum;](#第 7 行:return sum;)
前言:
这是一个一个经典的树形动态规划(Tree DP)问题,通常被称为 "最大子树和" 或 "带权树的最大连通子图和"
采用动态规划和贪心算法!
题目:
在 X 森林里,上帝创建了生命之树。
他给每棵树的每个节点(叶子也称为一个节点)上,都标了一个整数,代表这个点的和谐值。
上帝要在这棵树内选出一个节点集 S,使得对于 SS 中的任意两个点 a,b,都存在一个点列 a,v1,v2,⋯,vk,b 使得这个点列中的每个点都是 S 里面的元素,且序列中相邻两个点间有一条边相连。
在这个前提下,上帝要使得 S 中的点所对应的整数的和尽量大。
这个最大的和就是上帝给生命之树的评分。
经过 atm 的努力,他已经知道了上帝给每棵树上每个节点上的整数。但是由于 atm 不擅长计算,他不知道怎样有效的求评分。他需要你为他写一个程序来计算一棵树的分数。
集合 S 可以为空。
输入描述
第一行一个整数 n 表示这棵树有 n 个节点。
第二行 n 个整数,依次表示每个节点的评分。
接下来 n−1 行,每行 2 个整数 u,v,表示存在一条 u 到 v 的边。由于这是一棵树,所以是不存在环的。
其中,0<n≤105, 每个节点的评分的绝对值不超过 106。
输出描述
输出一行一个数,表示上帝给这棵树的分数。
输入输出样例
示例
输入
5 1 -2 -3 4 5 4 2 3 1 1 2 2 5
输出
8
运行限制
- 最大运行时间:3s
- 最大运行内存: 256M
题目分析:
一、题目描述
给定一棵包含 n 个节点的无向树,每个节点有一个整数值(可正可负)。
要求选择一个连通的子图(即若干相连的节点),使得这些节点的权值之和最大。返回这个最大和。
二、问题
- 输入结构:一棵树(无环连通无向图),可用邻接表表示。
- 目标:找一个连通的节点集合,使其权值和最大。
- 关键约束:所选节点必须构成连通子图(不能跳着选)。
注意:这不是"最大独立集",也不是"最长路径",而是带权最大连通子图。
三、观察
- 树的性质:任意两个节点间有唯一路径 → 任何连通子图也是一棵树。
- 最优解必有"根":最大连通子图一定存在一个"最高点"(在 DFS 遍历时最先被访问的节点)。
- 局部决策影响全局:若某个子树的总贡献为负,则不应纳入当前解。
四、算法步骤
- 构建邻接表存储树。
- 从任意节点(如 1)开始 DFS。
- 对每个节点:
- 初始化当前和为自身权值;
- 递归处理子节点;
- 仅累加正贡献的子树;
- 更新全局最大值;
- 返回当前和供父节点使用。
- 输出
maxSum。
代码:
javapackage com.itdonghuang.Test; import java.util.*; public class JavaTest1 { static long maxsum = 0; public static void main(String[] args) { Scanner scan = new Scanner(System.in); int n = scan.nextInt(); int[] val = new int[n + 1]; for (int i = 1; i <= n; i++) { val[i] = scan.nextInt(); } List<Integer>[] opp = new ArrayList[n + 1]; for (int i = 1; i <= n; i++) { opp[i] = new ArrayList<>(); } for (int i = 0; i < n - 1; i++) { int u = scan.nextInt(); int v = scan.nextInt(); opp[u].add(v); opp[v].add(u); } dfs(1, -1, opp, val); System.out.println(maxsum); scan.close(); } public static long dfs(int u, int parent, List<Integer>[] opp, int[] val) { long sum = val[u]; for (int v : opp[u]) { if (v == parent) continue; long childSum = dfs(v, u, opp, val); if (childSum > 0) { sum += childSum; } } if (sum > maxsum) { maxsum = sum; } return sum; } }
代码分析:
一、初始化,赋值
定义n、val、opp分别接受键盘输入的节点数、每个结点的评分、u<-->v的边
opp是一个数组array,每个元素是List<Integer>,构成无向图
javaScanner scan = new Scanner(System.in); int n = scan.nextInt(); int[] val = new int[n + 1]; for (int i = 1; i <= n; i++) { val[i] = scan.nextInt(); } List<Integer>[] opp = new ArrayList[n + 1]; for (int i = 1; i <= n; i++) { opp[i] = new ArrayList<>(); } for (int i = 0; i < n - 1; i++) { int u = scan.nextInt(); int v = scan.nextInt(); opp[u].add(v); opp[v].add(u); }
二、dfs方法讲解
逐行详解
第 1 行:long sum = val[u];
- 含义 :当前子树至少包含节点
u自己。 - **用
long,**防止整数溢出(权值可能很大)。 - 关键思想 :我们计算的是 "必须包含当前节点 u 的最大连通子图和"。
注意:这个子图必须包含
u,但可以选择性地包含它的某些子树。
第 2--3 行:遍历邻居 + 跳过父节点
javafor (int v : opp[u]) { if (v == parent) continue;
opp[u]是节点u的所有邻居(来自邻接表)。- 因为树是无向图,
u的邻居包括它的父节点 和子节点。 - 但我们是从根往下 DFS 的,所以要避免走回父节点,否则会无限递归或重复访问。
举例:
如果从
1 → 2 → 3,那么在dfs(2, 1)中,opp[2]包含1和3,必须跳过
1(因为它是父节点),只处理3。
第 4 行:long childSum = dfs(v, u, opp, val);
- 递归调用 :进入子节点
v,并告诉它:"你的父节点是u"。 - 返回值含义 :
childSum= 以v为根的子树中,包含v的最大连通子图的和。
再次强调:这个值必须包含
v,但可能只包含v自己(如果子树都是负的)。
第 5 行:if (childSum > 0) sum += childSum;
这是整个算法最核心的贪心思想!
为什么只加正数?
- 如果某个子树的最大和是 负数 (比如
-5),把它加到当前节点只会让总和变小。 - 所以我们只"吸收"那些能带来正收益的子树。
- 这相当于:不选那些负贡献的子树分支。
类比数组版"最大子数组和"(Kadane 算法):
如果前缀和 < 0,就丢掉,重新开始。
这里同理:如果子树和 < 0,就"断开",不选它。
举个例子:
val[u] = 3- 子树 A 贡献
+4→ 加上 → 总和变成7 - 子树 B 贡献
-2→ 不加 → 总和还是7 - 最终
sum = 3 + 4 = 7
第 6 行:if (sum > maxsum) maxsum = sum;
- 更新全局答案!
sum是"包含当前节点u的最大连通子图和"。- 而全局最优解一定是以某个节点为"最高点"的连通子图(因为树是连通无环的)。
- 所以我们在每个节点都尝试一次,取最大值。
这就是为什么不需要额外判断"路径是否跨子树"------因为任何连通子图都有一个"顶部节点",我们会在那里计算它。
第 7 行:return sum;
- 返回值用途 :告诉父节点,"如果你把我(以 u 为根的这部分)接上去,最多能给你增加
sum的收益"。 - 父节点会根据这个值决定是否"吸收"你。
这是一个典型的 自底向上(bottom-up) 的信息传递过程。
javapublic static long dfs(int u, int parent, List<Integer>[] opp, int[] val) { long sum = val[u]; // 当前子树至少包含自己 for (int v : opp[u]) { if (v == parent) continue; // 避免回溯到父节点(防止死循环) long childSum = dfs(v, u, opp, val); // 递归计算以 v 为根的子树的最大"贡献值" if (childSum > 0) { sum += childSum; // 只有当子树贡献为正时,才合并进来 } } if (sum > maxsum) { maxsum = sum; // 更新全局最大子树和 } return sum; // 返回以 u 为根的子树能向上提供的最大和(用于父节点决策) }
结语:
今天是我刷算法的第N天,每天刷算法题,写写项目,补充知识点。完善知识体系,加油加油!希望可以帮助到你!
"你走的每一步,都算数;你坚持的每一刻,都在靠近光。" 💪✨