蓝桥杯--生命之树(Java)

目录

前言:

题目:

输入描述

输出描述

输入输出样例

运行限制

题目分析:

一、题目描述

二、问题

三、观察

四、算法步骤

代码:

代码分析:

一、初始化,赋值

二、dfs方法讲解

逐行详解

[第 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 个节点的无向树,每个节点有一个整数值(可正可负)。

要求选择一个连通的子图(即若干相连的节点),使得这些节点的权值之和最大。返回这个最大和。

二、问题

  • 输入结构:一棵树(无环连通无向图),可用邻接表表示。
  • 目标:找一个连通的节点集合,使其权值和最大。
  • 关键约束:所选节点必须构成连通子图(不能跳着选)。

注意:这不是"最大独立集",也不是"最长路径",而是带权最大连通子图。

三、观察

  1. 树的性质:任意两个节点间有唯一路径 → 任何连通子图也是一棵树。
  2. 最优解必有"根":最大连通子图一定存在一个"最高点"(在 DFS 遍历时最先被访问的节点)。
  3. 局部决策影响全局:若某个子树的总贡献为负,则不应纳入当前解。

四、算法步骤

  1. 构建邻接表存储树。
  2. 从任意节点(如 1)开始 DFS。
  3. 对每个节点:
    • 初始化当前和为自身权值;
    • 递归处理子节点;
    • 仅累加正贡献的子树;
    • 更新全局最大值;
    • 返回当前和供父节点使用。
  4. 输出 maxSum

代码:

java 复制代码
package 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>,构成无向图

java 复制代码
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 行:long sum = val[u];

  • 含义 :当前子树至少包含节点 u 自己。
  • **用 long,**防止整数溢出(权值可能很大)。
  • 关键思想 :我们计算的是 "必须包含当前节点 u 的最大连通子图和"

注意:这个子图必须包含 u,但可以选择性地包含它的某些子树。


第 2--3 行:遍历邻居 + 跳过父节点

java 复制代码
for (int v : opp[u]) {
    if (v == parent) continue;
  • opp[u] 是节点 u 的所有邻居(来自邻接表)。
  • 因为树是无向图,u 的邻居包括它的父节点子节点
  • 但我们是从根往下 DFS 的,所以要避免走回父节点,否则会无限递归或重复访问。

举例:

如果从 1 → 2 → 3,那么在 dfs(2, 1) 中,opp[2] 包含 13

必须跳过 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) 的信息传递过程。

java 复制代码
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); // 递归计算以 v 为根的子树的最大"贡献值"

        if (childSum > 0) {
            sum += childSum; // 只有当子树贡献为正时,才合并进来
        }
    }

    if (sum > maxsum) {
        maxsum = sum; // 更新全局最大子树和
    }

    return sum; // 返回以 u 为根的子树能向上提供的最大和(用于父节点决策)
}

结语:

今天是我刷算法的第N天,每天刷算法题,写写项目,补充知识点。完善知识体系,加油加油!希望可以帮助到你!

"你走的每一步,都算数;你坚持的每一刻,都在靠近光。" 💪✨

相关推荐
醉颜凉16 小时前
【LeetCode】打家劫舍III
c语言·算法·leetcode·树 深度优先搜索·动态规划 二叉树
达文汐16 小时前
【困难】力扣算法题解析LeetCode332:重新安排行程
java·数据结构·经验分享·算法·leetcode·力扣
一匹电信狗16 小时前
【LeetCode_21】合并两个有序链表
c语言·开发语言·数据结构·c++·算法·leetcode·stl
User_芊芊君子16 小时前
【LeetCode经典题解】搞定二叉树最近公共祖先:递归法+栈存路径法,附代码实现
算法·leetcode·职场和发展
培风图南以星河揽胜16 小时前
Java版LeetCode热题100之零钱兑换:动态规划经典问题深度解析
java·leetcode·动态规划
算法_小学生16 小时前
LeetCode 热题 100(分享最简单易懂的Python代码!)
python·算法·leetcode
执着25916 小时前
力扣hot100 - 234、回文链表
算法·leetcode·链表
Gorgous—l16 小时前
数据结构算法学习:LeetCode热题100-多维动态规划篇(不同路径、最小路径和、最长回文子串、最长公共子序列、编辑距离)
数据结构·学习·算法
熬夜造bug16 小时前
LeetCode Hot100 刷题路线(Python版)
算法·leetcode·职场和发展
启山智软16 小时前
【中大企业选择源码部署商城系统】
java·spring·商城开发