leetcode
leetcode 1301 ~ 1350
收集树上所有苹果的最少时间

收集树上所有苹果的最少时间

难度:

标签:

题目描述

给你一棵有 n 个节点的无向树,节点编号为 0 到 n-1 ,它们中有一些节点有苹果。通过树上的一条边,需要花费 1 秒钟。你从 节点 0 出发,请你返回最少需要多少秒,可以收集到所有苹果,并回到节点 0 。

无向树的边由 edges 给出,其中 edges[i] = [fromi, toi] ,表示有一条边连接 from 和 toi 。除此以外,还有一个布尔数组 hasApple ,其中 hasApple[i] = true 代表节点 i 有一个苹果,否则,节点 i 没有苹果。

 

示例 1:

输入:n = 7, edges = [[0,1],[0,2],[1,4],[1,5],[2,3],[2,6]], hasApple = [false,false,true,false,true,true,false]
输出:8 
解释:上图展示了给定的树,其中红色节点表示有苹果。一个能收集到所有苹果的最优方案由绿色箭头表示。

示例 2:

输入:n = 7, edges = [[0,1],[0,2],[1,4],[1,5],[2,3],[2,6]], hasApple = [false,false,true,false,false,true,false]
输出:6
解释:上图展示了给定的树,其中红色节点表示有苹果。一个能收集到所有苹果的最优方案由绿色箭头表示。

示例 3:

输入:n = 7, edges = [[0,1],[0,2],[1,4],[1,5],[2,3],[2,6]], hasApple = [false,false,false,false,false,false,false]
输出:0

 

提示:

  • 1 <= n <= 10^5
  • edges.length == n - 1
  • edges[i].length == 2
  • 0 <= ai < bi <= n - 1
  • hasApple.length == n

代码结果

运行时间: 104 ms, 内存: 45.0 MB


/*
 * 思路:
 * 1. 构建树的表示,使用邻接表。
 * 2. 使用深度优先搜索(DFS)来遍历树。
 * 3. 对于每个节点,如果其子树中有苹果,计算需要的时间并返回。
 * 4. 如果一个节点或其子节点有苹果,增加相应的时间。
 * 5. 使用Java Stream API来构建树和进行DFS遍历。
 */

import java.util.*;
import java.util.stream.*;

public class SolutionStream {
    public int minTime(int n, int[][] edges, List<Boolean> hasApple) {
        // 构建邻接表
        Map<Integer, List<Integer>> tree = Arrays.stream(edges)
                .flatMap(edge -> Stream.of(Map.entry(edge[0], edge[1]), Map.entry(edge[1], edge[0])))
                .collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.toList())));
        // 记录访问节点避免重复
        boolean[] visited = new boolean[n];
        return dfs(0, tree, hasApple, visited);
    }

    private int dfs(int node, Map<Integer, List<Integer>> tree, List<Boolean> hasApple, boolean[] visited) {
        visited[node] = true;
        int totalTime = tree.getOrDefault(node, Collections.emptyList()).stream()
                .filter(child -> !visited[child])
                .mapToInt(child -> {
                    int timeSpent = dfs(child, tree, hasApple, visited);
                    return (timeSpent > 0 || hasApple.get(child)) ? timeSpent + 2 : 0;
                })
                .sum();
        return totalTime;
    }
}

解释

方法:

题解使用深度优先搜索(DFS)策略,递归地计算树中每个节点所需要的最小时间。基本思路是:从根节点(节点 0)开始,向下遍历每个节点,计算从该节点到其所有子节点收集苹果并返回所需的总时间。如果一个节点或其任何子节点有苹果,该节点返回的时间包括到子节点的往返时间(每个边2秒)。若叶子节点有苹果,则计算其到父节点的往返时间。若叶子节点无苹果,则不需要时间。对于根节点,只需要累加从根节点到其子节点的时间,因为不需要返回根节点。

时间复杂度:

O(n)

空间复杂度:

O(n)

代码细节讲解

🦆
在DFS实现中,为什么要传递父节点的编号`fa`到每次递归调用中?
在深度优先搜索(DFS)中传递父节点编号`fa`是为了防止算法回溯到父节点,从而导致无限循环。由于图的邻接表表示中,每个节点都记录了与它相连的所有节点(包括父节点),如果不通过父节点编号来区分,递归调用时会重新访问父节点,造成重复计算和循环调用。传递父节点编号可以确保每次递归只处理当前节点的子节点,而不包括其父节点。
🦆
如何确保不会重复访问同一个节点,避免造成无限循环?
在DFS的实现中,通过传递当前节点的父节点编号`fa`并在每次递归中检查,确保不会向父节点方向递归,从而避免重复访问同一个节点。此外,在递归函数中,每次遍历子节点前会检查该子节点是否是来自父节点的调用,只有当子节点不是父节点时,才会对该子节点进行递归调用。这样就可以确保算法在遍历过程中每个节点只被访问一次,避免了无限循环的问题。
🦆
在递归的基案中,为什么叶子节点的处理方式是返回`int(hasApple[u]) * 2`,这里的逻辑是什么?
在递归的基案中,叶子节点定义为除根节点外只与一个节点(其父节点)相连的节点。当叶子节点有苹果时,需要计算从该叶子节点到其父节点的往返时间,因此返回`2`(每个边的往返时间为2秒)。使用`int(hasApple[u]) * 2`是为了根据叶子节点是否有苹果来决定返回的时间:若有苹果(`hasApple[u]`为`true`),则返回`2`;若无苹果(`hasApple[u]`为`false`),则返回`0`。这样的处理确保了只有含有苹果的叶子节点才贡献额外的往返时间。

相关问题