Minimum Time to Collect All Apples in a Tree

Medium
11 days ago

Given an undirected tree consisting of n vertices numbered from 0 to n-1, which has some apples in their vertices. You spend 1 second to walk over one edge of the tree. Return the minimum time in seconds you have to spend to collect all apples in the tree, starting at vertex 0 and coming back to this vertex.

The edges of the undirected tree are given in the array edges, where edges[i] = [a<sub>i</sub>, b<sub>i</sub>] means that exists an edge connecting the vertices a<sub>i</sub> and b<sub>i</sub>. Additionally, there is a boolean array hasApple, where hasApple[i] = true means that vertex i has an apple; otherwise, it does not have any apple.

For example:

Input: n = 7, edges = [[0,1],[0,2],[1,4],[1,5],[2,3],[2,6]], hasApple = [false,false,true,false,true,true,false] Output: 8 Explanation: The figure above represents the given tree where red vertices have an apple. One optimal path to collect all apples is shown by the green arrows.

Input: n = 7, edges = [[0,1],[0,2],[1,4],[1,5],[2,3],[2,6]], hasApple = [false,false,true,false,false,true,false] Output: 6 Explanation: The figure above represents the given tree where red vertices have an apple. One optimal path to collect all apples is shown by the green arrows.

Input: n = 7, edges = [[0,1],[0,2],[1,4],[1,5],[2,3],[2,6]], hasApple = [false,false,false,false,false,false,false] Output: 0

Sample Answer
## Minimum Time to Collect All Apples in a Tree

This problem involves traversing a tree to collect apples and returning to the starting vertex. We need to find the minimum time (seconds) required to collect all apples, considering each edge traversal takes 1 second.

### Naive Approach (Brute Force)

1.  Explore all possible paths from vertex 0.
2.  For each path, check if it collects all apples.
3.  Calculate the time taken for each valid path (collects all apples and returns to 0).
4.  Return the minimum time among all valid paths.

This approach is highly inefficient due to the exponential number of possible paths in a tree.

### Optimal Approach (Depth-First Search - DFS)

We can use DFS to efficiently traverse the tree. For each subtree, we determine if it contains any apples (either in the root or in any of its children). If a subtree contains an apple, we need to include the path to that subtree (and back) in our total time. We only count the edges that lead to a vertex with an apple or a vertex that leads to another vertex with an apple.

**Algorithm:**

1.  Build an adjacency list to represent the tree.
2.  Implement a recursive DFS function:
    *   Base case: If the current vertex is a leaf node.
    *   Recursive step: For each child of the current vertex, recursively call the DFS function.
3.  In the DFS function, check if the current vertex has an apple or if any of its children require a path to collect apples.
4.  If either condition is true, add 2 to the total time (1 for going to the child and 1 for returning).

**Code (Java):**

```java
import java.util.ArrayList;
import java.util.List;

class Solution {
    public int minTime(int n, int[][] edges, List<Boolean> hasApple) {
        List<List<Integer>> adj = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            adj.add(new ArrayList<>());
        }
        for (int[] edge : edges) {
            adj.get(edge[0]).add(edge[1]);
            adj.get(edge[1]).add(edge[0]);
        }

        return dfs(0, -1, adj, hasApple);
    }

    private int dfs(int node, int parent, List<List<Integer>> adj, List<Boolean> hasApple) {
        int time = 0;
        for (int child : adj.get(node)) {
            if (child != parent) {
                int childTime = dfs(child, node, adj, hasApple);
                if (childTime > 0 || hasApple.get(child)) {
                    time += childTime + 2;
                }
            }
        }
        return time;
    }
}

Code (Python):

from collections import defaultdict
from typing import List

class Solution:
    def minTime(self, n: int, edges: List[List[int]], hasApple: List[bool]) -> int:
        adj = defaultdict(list)
        for u, v in edges:
            adj[u].append(v)
            adj[v].append(u)

        def dfs(node, parent):
            time = 0
            for child in adj[node]:
                if child != parent:
                    child_time = dfs(child, node)
                    if child_time > 0 or hasApple[child]:
                        time += child_time + 2
            return time

        return dfs(0, -1)

Big(O) Runtime Analysis

  • Time Complexity: O(N), where N is the number of vertices in the tree. We visit each node once during the DFS traversal.

    • The minTime function initializes an adjacency list. The time complexity is O(E), where E is the number of edges. In a tree, E = N-1, so this is O(N).
    • The dfs function is called recursively for each node in the tree. Each node is visited once because we keep track of the parent to avoid revisiting.
    • Inside the dfs function, the code iterates through the children of each node. In total, the number of iterations across all calls to dfs is proportional to the number of edges, which is O(N).
    • Therefore, the overall time complexity is dominated by the DFS traversal, which is O(N).

Big(O) Space Usage Analysis

  • Space Complexity: O(N), where N is the number of vertices in the tree. This is due to the adjacency list and the recursion stack during DFS.

    • The adjacency list adj stores the graph's structure. In the worst case, it can store all N-1 edges, leading to a space complexity of O(N).
    • The recursion stack for the dfs function can go as deep as the height of the tree. In the worst case (a skewed tree), the height can be N, leading to a space complexity of O(N) for the stack.
    • The hasApple list has a space complexity of O(N), but this is given input and not considered extra space.
    • Therefore, the overall space complexity is determined by the adjacency list and the recursion stack, resulting in O(N).

Edge Cases

  1. Empty Tree: If the tree is empty (n = 0), return 0.
  2. No Apples: If no vertices have apples, return 0.
  3. Root Has Apple Only: If only the root vertex (0) has an apple and there are no other vertices, return 0.
  4. Disconnected Graph: The problem statement specifies a tree, so we don't need to handle disconnected graphs.
  5. Single Node Tree: If n = 1, and hasApple[0] is true, return 0. If hasApple[0] is false, return 0.

These cases are handled correctly by the DFS approach. The base case ensures that if no children have apples and the current node doesn't either, no additional time is added.