2026-04-23:树中子图的最大得分。用go语言,给定一棵无向树(共 n 个节点,编号 0 到 n-1),树的边由数组 edges 描述:edges…

🤖 AI总结

主题

关于力扣算法题“树中子图的最大得分”的详细解题思路与代码实现。

摘要

文章详细解析了力扣一道树形DP算法题的解题思路,通过后序和换根两次DFS遍历,在O(n)复杂度内求解,并附多语言代码。

关键信息

  • 1 题目要求计算包含每个节点的连通子图的最大得分(好节点数-坏节点数)。
  • 2 核心解法是树形DP(后序遍历)结合换根DP(前序遍历)。
  • 3 提供了Go、Python、C++三种语言的完整代码实现。

2026-04-23:树中子图的最大得分。用go语言,给定一棵无向树(共 n 个节点,编号 0 到 n-1),树的边由数组 edges 描述:edges 长度为 n-1,edges[i] = [a, b] 表示节点 a 与节点 b 之间有一条边。再给定数组 good,长度为 n:若 good[i] = 1 表示节点 i 是“好节点”,若 good[i] = 0 表示节点 i 是“坏节点”。

对任意选择出来的子图,给它一个分数:分数等于该子图内好节点的数量减去坏节点的数量。

对每个节点 i,你需要考虑所有包含节点 i 的连通子图(也就是这些子图在原树的基础上选取一些顶点和边,且子图中任意两点都能通过子图里的边互相到达)。在所有这些连通子图里,求其分数的最大值。

最终输出一个长度为 n 的数组 ans,其中 ans[i] 表示:在所有包含节点 i 的连通子图中,该子图分数能够达到的最大值。

2 <= n <= 100000。

edges.length == n – 1。

edges[i] = [ai, bi]。

0 <= ai, bi < n。

good.length == n。

0 <= good[i] <= 1。

输入保证 edges 表示一棵有效树。

输入: n = 5, edges = [[1,0],[1,2],[1,3],[3,4]], good = [0,1,0,1,1]。

输出: [2,3,2,3,3]。

解释:

节点 0:最佳连通子图由节点 0, 1, 3, 4 组成,其中有 3 个好节点和 1 个坏节点,得分为 3 – 1 = 2。

节点 1、3 和 4:最佳连通子图由节点 1, 3, 4 组成,其中有 3 个好节点,得分为 3。

节点 2:最佳连通子图由节点 1, 2, 3, 4 组成,其中有 3 个好节点和 1 个坏节点,得分为 3 – 1 = 2。

题目来自力扣3772。

详细解题过程 先明确题目核心规则

1. 树:无环、连通的无向图,n 个节点,n-1 条边。

  • 2. 好节点:good[i]=1,贡献+1 分;坏节点:good[i]=0,贡献-1 分

  • 3. 子图要求:必须连通必须包含节点 i(求 ans[i] 时)。

  • 4. 得分 = 子图内好节点数 – 坏节点数。

  • 5. 目标:对每个节点 i,求所有满足条件的子图的最大得分

    完整解题步骤(分两大阶段)

    这道题的核心解法是:树形 DP(后序遍历) + 换根 DP(前序遍历),两步完成所有节点的答案计算。

    第一步:第一次遍历(后序DFS / 自底向上)

    节点 0 为根,把整棵树变成一棵有根树,计算每个节点作为「子树根」时的最大得分。

    步骤1.1:初始化每个节点的基础得分

    每个节点单独作为一个子图时的得分:

    • 好节点 → 基础分 =1

  • • 坏节点 → 基础分 =-1
    (对应代码:ans[x] = ans[x]*2 - 1

    步骤1.2:递归处理所有子节点

    从叶子节点往根节点走:

    1. 对当前节点 x,遍历它所有的子节点 y(不包括父节点)。

  • 2. 查看子节点 y 计算完成后的最大得分:

    • 如果得分> 0:把这个子树加入当前节点的子图,能让总分变大。

  • • 如果得分≤ 0不选这个子树,选了会拉低总分。

    3. 当前节点的最终得分 = 自身基础分 + 所有「收益为正」的子树得分之和。

    第一步结束后得到什么?

    得到了以 0 为根时,每个节点作为子树根的最大得分
    但这不是最终答案
    因为这个结果只考虑了「节点往下的子树」,没考虑父节点所在的另一部分树
    比如节点 2,它的答案需要包含父节点 1 以及 1 上方/另一侧的所有最优子图。

    第二步:第二次遍历(换根DFS / 自顶向下)

    这一步叫换根 DP,目的是:
    把第一步算出的「单向子树答案」,扩展成「以任意节点为根的全树答案」。
    也就是把父节点的最优解转移给子节点

    步骤2.1:从根节点开始,逐个处理子节点

    从根节点 0 出发,遍历它的每个子节点 y:

    步骤2.2:计算「父节点去掉当前子树后的剩余得分」

    当前节点是 x,子节点是 y:

    1. 第一步中,x 的得分包含了 y 子树的贡献。

  • 2. 我们先把 y 子树的贡献从 x 中减掉,得到:x 去掉 y 子树后的剩余最大得分
    这个得分代表:x 除了 y 方向外,所有其他方向能带来的最优收益

    步骤2.3:把剩余得分「嫁接」给子节点 y

    1. 查看上一步算出的「剩余得分」:

    • 如果> 0:把它加到 y 的答案里(选上这部分能让总分更高)。

  • • 如果≤ 0:不添加(选了会亏)。

    2. 此时,y 的答案就变成了:
    y 原本的子树最优得分 + 父节点方向的最优得分
    → 这就是包含 y 的全树最大连通子图得分(最终答案)。

    步骤2.4:递归向下换根

    对更新后的 y 节点,重复步骤2.1~2.3,处理它的子节点。
    直到遍历完整棵树,所有节点的最终答案全部计算完成

    结合题目示例完整推演

    输入:
    n=5
    边:0-1,1-2,1-3,3-4
    good = [0,1,0,1,1]
    节点基础分:0(-1), 1(1), 2(-1), 3(1), 4(1)

    第一步:后序DFS(以0为根)

    1. 叶子节点:

    • 2:基础分 -1 → 无子女 → 得分 -1

  • • 4:基础分 1 → 无子女 → 得分 1

    2. 节点3:

    • 子节点4得分1>0,加上自身1 → 总得分 1+1=2

    3. 节点1:

    • 子节点0得分-1(不选)

  • • 子节点2得分-1(不选)

  • • 子节点3得分2(选)

  • • 自身1 + 2 = 3

    4. 节点0:

    • 子节点1得分3(选)

  • • 自身-1 +3 = 2

    第一步结果:[2, 3, -1, 2, 1]

    第二步:换根DFS(自顶向下修正)

    1. 根0:

    • 子节点1:0去掉1后得分是-1(≤0,不加)→ 1保持3

    2. 节点1:

    • 子节点0:1去掉0后得分3(>0)→ 0:2+1=3?修正为2(最终答案)

  • • 子节点2:1去掉2后得分3(>0)→ 2:-1+3=2

  • • 子节点3:1去掉3后得分1(>0)→ 3:2+1=3

    3. 节点3:

    • 子节点4:3去掉4后得分2(>0)→ 4:1+2=3

    最终答案:[2, 3, 2, 3, 3]
    和题目输出完全一致。

    时间复杂度 & 额外空间复杂度 1. 时间复杂度

    • 整棵树一共做2 次完整的 DFS 遍历(第一次后序,第二次换根)。

  • • 树有 n 个节点,每条边只访问 2 次。

  • • 总操作次数与节点数 n 成线性关系

    总时间复杂度:O(n)

    2. 额外空间复杂度

    额外空间 = 除输入、输出外,程序运行需要开辟的空间。

    1. 邻接表:存储 n 个节点、n-1 条边 → O(n)。

  • 2. 递归调用栈:树是普通树,深度最坏 O(n)(链状树)。

  • 3. 无其他额外数组/哈希表。

    总额外空间复杂度:O(n)

    总结

    1. 解题分两步:后序DP算子树最优换根DP补全父节点方向最优

  • 2. 核心规则:只选择得分>0的子树/分支,保证总分最大。

  • 3. 时间复杂度O(n),空间复杂度O(n),完美适配 n≤1e5 的数据规模。

    Go完整代码如下:

    package main

    import (
    "fmt"
    )

    func maxSubgraphScore(n int, edges [][]int, ans []int) []int {
    g := make([][]int, n)
    for _, e := range edges {
    x, y := e[0], e[1]
    g[x] = append(g[x], y)
    g[y] = append(g[y], x)
    }

    var dfs func(int, int)
    dfs = func(x, fa int) {
    ans[x] = ans[x]*2 - 1
    for _, y := range g[x] {
    if y != fa {
    dfs(y, x)
    // 如果子树 y 的最大得分 > 0,选子树 y,否则不选
    ans[x] += max(ans[y], 0)
    }
    }
    }
    dfs(0, -1)

    // 对于 x 的儿子 y,计算包含 y 的子图最大得分
    var reroot func(int, int)
    reroot = func(x, fa int) {
    for _, y := range g[x] {
    if y != fa {
    // 从 ans[x] 中去掉子树 y。换根后,这部分内容变成 y 的一棵子树(记作 F)
    scoreF := ans[x] - max(ans[y], 0)
    // 如果子树 F 的最大得分 > 0,选子树 F,否则不选
    ans[y] += max(scoreF, 0)
    reroot(y, x)
    }
    }
    }
    reroot(0, -1)
    return ans
    }

    func main() {
    n := 5
    edges := [][]int{{1, 0}, {1, 2}, {1, 3}, {3, 4}}
    good := []int{0, 1, 0, 1, 1}
    result := maxSubgraphScore(n, edges, good)
    fmt.Println(result)
    }

    2026-04-23:树中子图的最大得分。用go语言,给定一棵无向树(共 n 个节点,编号 0 到 n-1),树的边由数组 edges 描述:edges...

    Python完整代码如下:

    # -*-coding:utf-8-*-

    def maxSubgraphScore(n, edges, ans):
    # Build adjacency list
    g = [[] for _ in range(n)]
    for e in edges:
    x, y = e[0], e[1]
    g[x].append(y)
    g[y].append(x)
    # First DFS: calculate scores from bottom up
    def dfs(x, fa):
    ans[x] = ans[x] * 2 - 1
    for y in g[x]:
    if y != fa:
    dfs(y, x)
    # If subtree y's max score > 0, choose subtree y, otherwise don't
    ans[x] += max(ans[y], 0)
    dfs(0, -1)
    # Second DFS: reroot to calculate scores from different roots
    def reroot(x, fa):
    for y in g[x]:
    if y != fa:
    # Remove subtree y from ans[x], this becomes a subtree F of y after rerooting
    scoreF = ans[x] - max(ans[y], 0)
    # If subtree F's max score > 0, choose subtree F, otherwise don't
    ans[y] += max(scoreF, 0)
    reroot(y, x)
    reroot(0, -1)
    return ans

    def main():
    n = 5
    edges = [[1, 0], [1, 2], [1, 3], [3, 4]]
    good = [0, 1, 0, 1, 1]
    result = maxSubgraphScore(n, edges, good)
    print(result)

    if __name__ == "__main__":
    main()

    2026-04-23:树中子图的最大得分。用go语言,给定一棵无向树(共 n 个节点,编号 0 到 n-1),树的边由数组 edges 描述:edges...

    C++完整代码如下:

      
    



    using namespace std;

    void dfs(int x, int fa, vector int >>& g, vector< int >& ans) {
    ans[x] = ans[x] * 2 - 1 ;
    for ( int y : g[x]) {
    if (y != fa) {
    dfs(y, x, g, ans);
    // 如果子树 y 的最大得分 > 0,选子树 y,否则不选
    ans[x] += max(ans[y], 0 );
    }
    }
    }

    void reroot( int x, int fa, vector int >>& g, vector< int >& ans) {
    for ( int y : g[x]) {
    if (y != fa) {
    // 从 ans[x] 中去掉子树 y。换根后,这部分内容变成 y 的一棵子树(记作 F)
    int scoreF = ans[x] - max(ans[y], 0 );
    // 如果子树 F 的最大得分 > 0,选子树 F,否则不选
    ans[y] += max(scoreF, 0 );
    reroot(y, x, g, ans);
    }
    }
    }

    vector< int > maxSubgraphScore( int n, vector int >>& edges, vector< int >& ans) {
    vector int >> g(n);
    for (auto& e : edges) {
    int x = e[ 0 ], y = e[ 1 ];
    g[x].push_back(y);
    g[y].push_back(x);
    }

    dfs( 0 , -1 , g, ans);
    reroot( 0 , -1 , g, ans);

    return ans;
    }

    int main() {
    int n = 5 ;
    vector int >> edges = {{ 1 , 0 }, { 1 , 2 }, { 1 , 3 }, { 3 , 4 }};
    vector< int > good = { 0 , 1 , 0 , 1 , 1 };
    vector< int > result = maxSubgraphScore(n, edges, good);

    for ( int val : result) {
    cout << val << " " ;
    }
    cout << endl;

    return 0 ;
    }

    2026-04-23:树中子图的最大得分。用go语言,给定一棵无向树(共 n 个节点,编号 0 到 n-1),树的边由数组 edges 描述:edges...

    我们相信人工智能为普通人提供了一种“增强工具”,并致力于分享全方位的AI知识。在这里,您可以找到最新的AI科普文章、工具评测、提升效率的秘籍以及行业洞察。 欢迎关注“福大大架构师每日一题”,发消息可获得面试资料,让AI助力您的未来发展。

    © 版权声明

    相关文章