蓝桥杯--生命之树(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天,每天刷算法题,写写项目,补充知识点。完善知识体系,加油加油!希望可以帮助到你!

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

相关推荐
多则惑少则明2 小时前
AI大模型实用(九)Java快速实现智能体整理(使用LangChain4j-agentic + Tool)
java·人工智能·springai·langchain4j
与遨游于天地2 小时前
深入了解 Java `synchronized`:从对象头到锁升级、线程竞争感知
java·开发语言·c#
天天摸鱼的java工程师2 小时前
Kafka 消息积压处理实战:百万级队列清空的优化技巧
java·后端
MobotStone2 小时前
三步高效拆解顶刊论文
算法
CreasyChan2 小时前
unity射线与几何检测 - “与世界的交互”
算法·游戏·3d·unity·数学基础
东东的脑洞2 小时前
【面试突击四】JAVA基础知识-线程池与参数调优
java·面试
小股虫2 小时前
Tair Java实操手册:从零开始的缓存中间件入门指南
java·缓存·中间件
Wyy_9527*2 小时前
Spring三种注入方式对比
java·后端·spring
shepherd1112 小时前
从入门到实践:玩转分布式链路追踪利器SkyWalking
java·后端·架构