1443. Minimum Time to Collect All Apples in a Tree
Problem Description
You have an undirected tree with n
vertices numbered from 0
to n-1
. Some vertices contain apples. Starting from vertex 0
, you need to collect all apples and return to vertex 0
. Walking along any edge takes exactly 1 second.
The tree structure is defined by:
edges
: An array whereedges[i] = [ai, bi]
represents an undirected edge between verticesai
andbi
hasApple
: A boolean array wherehasApple[i] = true
if vertexi
has an apple,false
otherwise
Your task is to find the minimum time (in seconds) needed to collect all apples and return to the starting vertex 0
.
The solution uses a depth-first search (DFS) approach to traverse the tree. The key insight is that you only need to visit a subtree if it contains apples. The algorithm:
- Builds an adjacency list representation of the tree from the edges
- Uses DFS starting from vertex
0
with a visited array to avoid revisiting nodes - For each vertex, recursively explores all unvisited neighbors
- Each edge that needs to be traversed contributes 2 seconds to the total time (going down and coming back)
- A subtree is only explored if it contains apples (either at the current node or in its descendants)
- Returns
0
for a subtree if it has no apples and none of its children have apples - Otherwise, returns the cost (2 seconds per edge) plus the cumulative cost from child subtrees
The cost
parameter in the DFS function represents the cost of reaching the current node from its parent (0 for root, 2 for all other nodes since each edge costs 2 seconds round-trip).
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem explicitly involves an undirected tree with vertices and edges. A tree is a special type of graph (connected and acyclic).
Is it a tree?
- Yes: The problem states we have an "undirected tree" with
n
vertices connected byn-1
edges (implied by the tree structure).
DFS
- Conclusion: Since we're dealing with a tree structure, the flowchart directs us to use DFS (Depth-First Search).
The DFS pattern is particularly suitable for this problem because:
- We need to explore the entire tree starting from vertex 0
- We need to determine which subtrees contain apples and should be visited
- We need to calculate the cumulative time from leaf nodes back to the root
- DFS naturally handles the recursive nature of collecting apples from subtrees and returning to the parent node
Conclusion: The flowchart suggests using a DFS approach for traversing the tree and collecting all apples, which aligns perfectly with the solution that uses depth-first search to explore only the necessary paths containing apples.
Intuition
The key insight is to think about what paths we actually need to traverse. Since we start at vertex 0 and must return to vertex 0, any edge we traverse must be walked twice - once going down and once coming back up. This means each edge we use costs 2 seconds.
The crucial question becomes: which edges do we need to traverse? We only need to visit a subtree if it contains at least one apple. If a subtree has no apples at all, we can skip it entirely.
Think of it like collecting items in a house with multiple rooms and hallways. You wouldn't walk into a wing of the house that has no items to collect - you'd only visit rooms and hallways that lead to items or are on the path to items.
This naturally leads to a bottom-up approach using DFS:
- Start from the root and recursively explore each subtree
- For each leaf node, check if it has an apple
- As we backtrack, a parent node needs to be visited if either:
- It has an apple itself (
hasApple[node] = true
) - Any of its children's subtrees contain apples (indicated by non-zero return value from child DFS calls)
- It has an apple itself (
The beauty of this approach is that DFS automatically handles the backtracking for us. When we explore a child and it returns a non-zero value (meaning that subtree has apples), we know we must include the edge to that child in our path, adding 2 seconds to our total time.
The visited array prevents us from going back to the parent node during traversal (since the tree is represented as an undirected graph), ensuring we only explore each subtree once.
Learn more about Tree, Depth-First Search and Breadth-First Search patterns.
Solution Approach
The implementation uses DFS with a visited array to traverse the tree and calculate the minimum time needed.
Data Structures Used:
g
: An adjacency list (usingdefaultdict(list)
) to represent the undirected treevis
: A boolean array to track visited nodes and prevent revisiting
Building the Graph:
g = defaultdict(list)
for u, v in edges:
g[u].append(v)
g[v].append(u)
Since the tree is undirected, we add edges in both directions.
DFS Implementation:
The recursive dfs(u, cost)
function takes two parameters:
u
: Current vertex being exploredcost
: The cost to reach this vertex from its parent (0 for root, 2 for others)
Core Algorithm Logic:
-
Base Case - Already Visited:
if vis[u]: return 0
If we've already visited this node, return 0 to avoid cycles.
-
Mark as Visited:
vis[u] = True
-
Explore Children:
nxt_cost = 0 for v in g[u]: nxt_cost += dfs(v, 2)
Recursively explore all neighbors (children in the tree context). Each child is reached with cost 2 (1 second down + 1 second back). The
nxt_cost
accumulates the total cost from all child subtrees. -
Pruning Decision:
if not hasApple[u] and nxt_cost == 0: return 0
If the current node has no apple AND none of its children's subtrees have apples (indicated by
nxt_cost == 0
), this entire subtree can be skipped. -
Return Total Cost:
return cost + nxt_cost
Otherwise, return the cost to reach this node plus the accumulated cost from child subtrees.
Starting the Traversal:
return dfs(0, 0)
Start DFS from vertex 0 with initial cost 0 (since we're already at the starting point).
The algorithm efficiently prunes unnecessary branches while ensuring all apple-containing paths are traversed exactly once in each direction.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Example Tree:
n = 7 edges = [[0,1],[0,2],[1,4],[1,5],[2,3],[2,6]] hasApple = [false,false,true,false,true,true,false]
This creates the following tree structure:
0 / \ 1 2 / \ / \ 4 5 3 6
Where apples are at vertices: 2, 4, and 5 (marked with asterisks below)
0 / \ 1 2* / \ / \ 4* 5* 3 6
Step-by-step DFS traversal:
-
Start at vertex 0 with
dfs(0, 0)
:- Mark vertex 0 as visited
- Explore children: vertices 1 and 2
-
Explore vertex 1 with
dfs(1, 2)
:- Mark vertex 1 as visited
- Cost to reach from parent = 2
- Explore children: vertices 4 and 5
-
Explore vertex 4 with
dfs(4, 2)
:- Mark vertex 4 as visited
- Cost to reach from parent = 2
- No unvisited children, so
nxt_cost = 0
- Has apple (
hasApple[4] = true
), so return2 + 0 = 2
-
Explore vertex 5 with
dfs(5, 2)
:- Mark vertex 5 as visited
- Cost to reach from parent = 2
- No unvisited children, so
nxt_cost = 0
- Has apple (
hasApple[5] = true
), so return2 + 0 = 2
-
Back to vertex 1:
nxt_cost = 2 + 2 = 4
(sum from vertices 4 and 5)- No apple at vertex 1, but
nxt_cost > 0
- Return
2 + 4 = 6
-
Explore vertex 2 with
dfs(2, 2)
:- Mark vertex 2 as visited
- Cost to reach from parent = 2
- Explore children: vertices 3 and 6
-
Explore vertex 3 with
dfs(3, 2)
:- Mark vertex 3 as visited
- Cost to reach from parent = 2
- No unvisited children, so
nxt_cost = 0
- No apple (
hasApple[3] = false
) andnxt_cost = 0
- Return
0
(this subtree is pruned!)
-
Explore vertex 6 with
dfs(6, 2)
:- Mark vertex 6 as visited
- Cost to reach from parent = 2
- No unvisited children, so
nxt_cost = 0
- No apple (
hasApple[6] = false
) andnxt_cost = 0
- Return
0
(this subtree is pruned!)
-
Back to vertex 2:
nxt_cost = 0 + 0 = 0
(both children pruned)- Has apple (
hasApple[2] = true
), so return2 + 0 = 2
-
Back to vertex 0:
nxt_cost = 6 + 2 = 8
(sum from vertices 1 and 2)- Initial cost was 0, so return
0 + 8 = 8
Final Result: 8 seconds
The path taken would be: 0→1→4→1→5→1→0→2→0, which uses 4 edges (0-1, 1-4, 1-5, 0-2), each traversed twice, giving us 4 × 2 = 8 seconds total.
Notice how vertices 3 and 6 were completely pruned since they had no apples and their subtrees had no apples. This pruning is what makes the algorithm efficient.
Solution Implementation
1from typing import List
2from collections import defaultdict
3
4class Solution:
5 def minTime(self, n: int, edges: List[List[int]], hasApple: List[bool]) -> int:
6 """
7 Calculate minimum time to collect all apples in a tree and return to root.
8
9 Args:
10 n: Number of nodes in the tree
11 edges: List of edges connecting nodes
12 hasApple: Boolean list indicating if node i has an apple
13
14 Returns:
15 Minimum time needed to collect all apples and return to node 0
16 """
17
18 def dfs(node: int, cost: int) -> int:
19 """
20 Depth-first search to calculate time needed from current node.
21
22 Args:
23 node: Current node being visited
24 cost: Cost to reach this node from parent (0 for root, 2 for others)
25
26 Returns:
27 Total time needed to collect apples in this subtree
28 """
29 # Skip if node already visited
30 if visited[node]:
31 return 0
32
33 # Mark current node as visited
34 visited[node] = True
35
36 # Calculate total cost from all children
37 children_cost = 0
38 for neighbor in graph[node]:
39 children_cost += dfs(neighbor, 2)
40
41 # If current node has no apple and no apples in subtree, return 0
42 if not hasApple[node] and children_cost == 0:
43 return 0
44
45 # Return cost to reach this node plus cost from children
46 return cost + children_cost
47
48 # Build adjacency list representation of the tree
49 graph = defaultdict(list)
50 for u, v in edges:
51 graph[u].append(v)
52 graph[v].append(u)
53
54 # Initialize visited array to track visited nodes
55 visited = [False] * n
56
57 # Start DFS from root node (0) with initial cost 0
58 return dfs(0, 0)
59
1class Solution {
2 /**
3 * Calculates minimum time to collect all apples in a tree and return to root
4 * @param n Number of nodes in the tree
5 * @param edges Array of edges representing tree connections
6 * @param hasApple List indicating whether each node has an apple
7 * @return Minimum time needed to collect all apples
8 */
9 public int minTime(int n, int[][] edges, List<Boolean> hasApple) {
10 // Track visited nodes to avoid cycles
11 boolean[] visited = new boolean[n];
12
13 // Build adjacency list representation of the tree
14 List<Integer>[] adjacencyList = new List[n];
15 Arrays.setAll(adjacencyList, index -> new ArrayList<>());
16
17 // Populate adjacency list with bidirectional edges
18 for (int[] edge : edges) {
19 int nodeU = edge[0];
20 int nodeV = edge[1];
21 adjacencyList[nodeU].add(nodeV);
22 adjacencyList[nodeV].add(nodeU);
23 }
24
25 // Start DFS from root node (0) with initial cost 0
26 return dfs(0, 0, adjacencyList, hasApple, visited);
27 }
28
29 /**
30 * Performs depth-first search to calculate minimum collection time
31 * @param currentNode Current node being visited
32 * @param edgeCost Cost to reach current node from parent (0 for root, 2 for others)
33 * @param adjacencyList Graph representation as adjacency list
34 * @param hasApple List indicating apple presence at each node
35 * @param visited Array tracking visited nodes
36 * @return Total time needed for subtree rooted at currentNode
37 */
38 private int dfs(int currentNode, int edgeCost, List<Integer>[] adjacencyList,
39 List<Boolean> hasApple, boolean[] visited) {
40 // Skip already visited nodes to prevent cycles
41 if (visited[currentNode]) {
42 return 0;
43 }
44
45 // Mark current node as visited
46 visited[currentNode] = true;
47
48 // Calculate total cost from all child subtrees
49 int childrenCost = 0;
50 for (int neighbor : adjacencyList[currentNode]) {
51 // Each edge costs 2 (going down and coming back)
52 childrenCost += dfs(neighbor, 2, adjacencyList, hasApple, visited);
53 }
54
55 // If current node has no apple and no apples in subtree, skip this path
56 if (!hasApple.get(currentNode) && childrenCost == 0) {
57 return 0;
58 }
59
60 // Return cost to reach this node plus cost of collecting from children
61 return edgeCost + childrenCost;
62 }
63}
64
1class Solution {
2public:
3 int minTime(int n, vector<vector<int>>& edges, vector<bool>& hasApple) {
4 // Track visited nodes to avoid cycles
5 vector<bool> visited(n, false);
6
7 // Build adjacency list representation of the tree
8 vector<vector<int>> adjacencyList(n);
9 for (const auto& edge : edges) {
10 int nodeU = edge[0];
11 int nodeV = edge[1];
12 adjacencyList[nodeU].push_back(nodeV);
13 adjacencyList[nodeV].push_back(nodeU);
14 }
15
16 // Start DFS from root node (0) with initial cost of 0
17 return dfs(0, 0, adjacencyList, hasApple, visited);
18 }
19
20private:
21 int dfs(int currentNode, int edgeCost, vector<vector<int>>& adjacencyList,
22 vector<bool>& hasApple, vector<bool>& visited) {
23 // Skip already visited nodes to avoid cycles
24 if (visited[currentNode]) {
25 return 0;
26 }
27
28 // Mark current node as visited
29 visited[currentNode] = true;
30
31 // Calculate total cost from all child subtrees
32 int childrenCost = 0;
33 for (const int& childNode : adjacencyList[currentNode]) {
34 // Each edge to a child costs 2 (going down and coming back)
35 childrenCost += dfs(childNode, 2, adjacencyList, hasApple, visited);
36 }
37
38 // If current node has no apple and no apples in its subtree, return 0
39 // This means we don't need to visit this subtree at all
40 if (!hasApple[currentNode] && childrenCost == 0) {
41 return 0;
42 }
43
44 // Return the cost to reach this node plus the cost of collecting apples in subtrees
45 return edgeCost + childrenCost;
46 }
47};
48
1function minTime(n: number, edges: number[][], hasApple: boolean[]): number {
2 // Track visited nodes to avoid cycles
3 const visited: boolean[] = new Array(n).fill(false);
4
5 // Build adjacency list representation of the tree
6 const adjacencyList: number[][] = Array.from({ length: n }, () => []);
7 for (const edge of edges) {
8 const nodeU = edge[0];
9 const nodeV = edge[1];
10 adjacencyList[nodeU].push(nodeV);
11 adjacencyList[nodeV].push(nodeU);
12 }
13
14 // Start DFS from root node (0) with initial cost of 0
15 return dfs(0, 0, adjacencyList, hasApple, visited);
16}
17
18function dfs(
19 currentNode: number,
20 edgeCost: number,
21 adjacencyList: number[][],
22 hasApple: boolean[],
23 visited: boolean[]
24): number {
25 // Skip already visited nodes to avoid cycles
26 if (visited[currentNode]) {
27 return 0;
28 }
29
30 // Mark current node as visited
31 visited[currentNode] = true;
32
33 // Calculate total cost from all child subtrees
34 let childrenCost = 0;
35 for (const childNode of adjacencyList[currentNode]) {
36 // Each edge to a child costs 2 (going down and coming back)
37 childrenCost += dfs(childNode, 2, adjacencyList, hasApple, visited);
38 }
39
40 // If current node has no apple and no apples in its subtree, return 0
41 // This means we don't need to visit this subtree at all
42 if (!hasApple[currentNode] && childrenCost === 0) {
43 return 0;
44 }
45
46 // Return the cost to reach this node plus the cost of collecting apples in subtrees
47 return edgeCost + childrenCost;
48}
49
Time and Space Complexity
Time Complexity: O(n)
The algorithm performs a depth-first search (DFS) traversal of the tree. Each node is visited exactly once due to the vis
array that marks visited nodes. For each node, we iterate through its adjacent nodes in the adjacency list. Since this is a tree with n
nodes and n-1
edges, and we store the edges bidirectionally, the total number of edges in the adjacency list is 2(n-1)
. The DFS visits each node once and processes each edge once, resulting in O(n + 2(n-1)) = O(n)
time complexity.
Space Complexity: O(n)
The space complexity consists of several components:
- The adjacency list
g
stores all edges bidirectionally, takingO(2(n-1)) = O(n)
space - The visited array
vis
takesO(n)
space - The recursive call stack in the worst case (when the tree is a linear chain) can go up to depth
n
, takingO(n)
space - The
hasApple
list is passed as input and not counted in auxiliary space
Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Handle the Undirected Nature of the Tree
Pitfall: A common mistake is treating the tree as directed and only adding edges in one direction when building the adjacency list. This would cause the DFS to miss certain branches of the tree.
# WRONG: Only adds edges in one direction
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v) # Missing the reverse edge!
Solution: Always add edges in both directions for undirected graphs:
# CORRECT: Add edges in both directions
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
2. Incorrect Cost Assignment for Non-Root Nodes
Pitfall: Assigning incorrect cost values when recursing to children. Some might mistakenly use cumulative costs or forget that each edge contributes 2 to the total (round trip).
# WRONG: Accumulating costs incorrectly
def dfs(node, accumulated_cost):
# ...
for neighbor in graph[node]:
children_cost += dfs(neighbor, accumulated_cost + 2) # Wrong!
Solution: The cost parameter should always be 2 for non-root nodes (representing the round trip on that edge), not an accumulated value:
# CORRECT: Fixed cost of 2 for each edge traversal
def dfs(node, cost):
# ...
for neighbor in graph[node]:
children_cost += dfs(neighbor, 2) # Always 2 for children
3. Missing the Visited Check Leading to Infinite Recursion
Pitfall: Without a visited array, the DFS will revisit nodes in the undirected tree, causing infinite recursion and stack overflow.
# WRONG: No visited tracking
def dfs(node, cost):
children_cost = 0
for neighbor in graph[node]:
children_cost += dfs(neighbor, 2) # Will revisit parent!
# ...
Solution: Always maintain and check a visited array:
# CORRECT: Track visited nodes
def dfs(node, cost):
if visited[node]:
return 0
visited[node] = True
# ... rest of the logic
4. Incorrect Pruning Logic
Pitfall: Pruning subtrees too aggressively or not pruning at all. A subtree should be explored if it has apples either at the current node OR in any descendant.
# WRONG: Only checks current node if not hasApple[node]: return 0 # Misses apples in descendants!
Solution: Check both the current node and the accumulated cost from children:
# CORRECT: Check both current node and descendants if not hasApple[node] and children_cost == 0: return 0 return cost + children_cost
5. Forgetting to Initialize the Visited Array
Pitfall: Using an uninitialized or incorrectly sized visited array can lead to index errors or incorrect behavior.
# WRONG: Forgot to initialize or wrong size visited = [] # Empty array # or visited = [False] * (n - 1) # Off by one error
Solution: Initialize the visited array with the correct size:
# CORRECT: Proper initialization visited = [False] * n
Which technique can we use to find the middle of a linked list?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Want a Structured Path to Master System Design Too? Don’t Miss This!