Amount of Time for Binary Tree to Be Infected
MedPrompt
You are given the root of a binary tree with unique values, and an integer start. At minute 0, an infection starts from the node with value start.
Each minute, a node becomes infected if:
- The node is currently uninfected.
- The node is adjacent to an infected node.
Return the number of minutes needed for the entire tree to be infected.
Example 1:
Input: root = [1,5,3,null,4,10,6,9,2], start = 3
Output: 4
Explanation: The following nodes are infected during:
- Minute 0: Node 3
- Minute 1: Nodes 1, 10 and 6
- Minute 2: Node 5
- Minute 3: Node 4
- Minute 4: Nodes 9 and 2
It takes 4 minutes for the whole tree to be infected so we return 4.Example 2:
Input: root = [1], start = 1
Output: 0
Explanation: At minute 0, the only node in the tree is infected so we return 0.
Constraints:
- The number of nodes in the tree is in the range
[1, 105]. 1 <= Node.val <= 105- Each node has a unique value.
- A node with a value of
startexists in the tree.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach treats the binary tree as an undirected graph. Since the infection spreads to adjacent nodes (parent, left child, right child), we can model this problem as finding the longest shortest path from the start node to any other node in the graph. The time taken is equal to this maximum distance. The algorithm first converts the tree into a more general graph data structure, like an adjacency list, and then performs a Breadth-First Search (BFS) starting from the infected node to find this maximum distance.
Algorithm
-
Step 1: Build Graph Representation.
- Create an adjacency list, for example, a
Map<Integer, List<Integer>>, to store the graph structure. - Traverse the binary tree using a recursive helper function (e.g., DFS).
- For each node, if it has a parent, add an undirected edge between the node and its parent in the adjacency list.
- Similarly, for each node, add undirected edges to its left and right children if they exist. A simpler way is to just traverse downwards and for each
nodewith achild, add(node.val, child.val)and(child.val, node.val)to the map.
- Create an adjacency list, for example, a
-
Step 2: Perform Breadth-First Search (BFS).
- Initialize a queue and add the
startnode's value. - Create a
visitedset to keep track of infected nodes, and add thestartvalue to it. - Initialize a variable
minutesto -1. - Begin a loop that continues as long as the queue is not empty. In each iteration, this loop processes one level of the graph (one minute of infection spread).
- Increment
minutes. - Get the number of nodes at the current level (
levelSize). - Loop
levelSizetimes to process each node at the current level. - Dequeue a node. For this node, iterate through its neighbors in the adjacency list.
- If a neighbor has not been visited, add it to the
visitedset and enqueue it. - Once the main loop finishes,
minuteswill hold the maximum time required, which is the answer.
- Initialize a queue and add the
Walkthrough
The core idea is to abstract away the tree structure into a graph, which simplifies reasoning about traversal in all directions (up, down, left, right).
First, we build the graph. We can traverse the tree with any method (like DFS). For each node, we establish connections to its neighbors. For any node u and its child v, we add an edge from u to v and another from v to u in our adjacency list. This ensures we can traverse both downwards and upwards.
private void buildGraph(TreeNode node, Map<Integer, List<Integer>> adj) { if (node == null) return; if (node.left != null) { adj.computeIfAbsent(node.val, k -> new ArrayList<>()).add(node.left.val); adj.computeIfAbsent(node.left.val, k -> new ArrayList<>()).add(node.val); } if (node.right != null) { adj.computeIfAbsent(node.val, k -> new ArrayList<>()).add(node.right.val); adj.computeIfAbsent(node.right.val, k -> new ArrayList<>()).add(node.val); } buildGraph(node.left, adj); buildGraph(node.right, adj);}Once the graph is constructed, we perform a standard level-order BFS starting from the start node. BFS is ideal here because it explores the graph layer by layer, which directly corresponds to the spread of infection minute by minute. We keep track of the number of levels traversed; this count will be our final answer.
public int amountOfTime(TreeNode root, int start) { Map<Integer, List<Integer>> adj = new HashMap<>(); buildGraph(root, adj); Queue<Integer> queue = new LinkedList<>(); queue.offer(start); Set<Integer> visited = new HashSet<>(); visited.add(start); int minutes = -1; while (!queue.isEmpty()) { minutes++; int levelSize = queue.size(); for (int i = 0; i < levelSize; i++) { int currentNode = queue.poll(); if (adj.containsKey(currentNode)) { for (int neighbor : adj.get(currentNode)) { if (!visited.contains(neighbor)) { visited.add(neighbor); queue.offer(neighbor); } } } } } return minutes;}Complexity
Time
O(N), where N is the number of nodes. Building the graph requires visiting each node once, which is O(N). The subsequent BFS also visits each node and edge once, which is O(V + E) = O(N + N-1) = O(N).
Space
O(N), where N is the number of nodes in the tree. The adjacency list stores 2*(N-1) edges, leading to O(N) space. The `visited` set and the BFS `queue` can also grow up to O(N) in the worst case.
Trade-offs
Pros
The logic is straightforward and follows a standard pattern for graph traversal problems.
It correctly models the problem by allowing traversal in any direction (up to parent, down to children).
Relatively easy to debug as the graph construction and BFS traversal are separate, well-understood algorithms.
Cons
Requires O(N) auxiliary space to store the adjacency list, which can be substantial for a large number of nodes.
Involves two distinct phases: building the graph and then traversing it, which might be slightly less performant in practice than a single-pass solution due to data structure overhead.
This optimized approach solves the problem in a single pass using Depth-First Search (DFS), thereby avoiding the O(N) space overhead of building an explicit graph. The key idea is to use the return value of the recursive DFS function to pass information up the tree. This information includes either the distance to the start node if it's in the current subtree, or the height of the current subtree if it's not. A global variable tracks the maximum infection time discovered.
Algorithm
- Define a global or class-level variable
maxTimeand initialize it to 0. - Create a recursive DFS function, say
traverse(node, start), that returns an integer with special meaning:- A positive return value
dsignifies that thestartnode was found in the currentnode's subtree, and its distance fromnodeisd. - A negative return value
-hsignifies that thestartnode was not found, and the height of the subtree rooted atnodeish. - A return value of
0corresponds to anullnode.
- A positive return value
- Base Case: If
traverseis called on anullnode, return0. - Recursive Step (Post-order Traversal):
- Recursively call
traverseon the left and right children to get their results:leftResultandrightResult. - Check if the current
nodeis thestartnode. Ifnode.val == start:- The infection starts here. The time to infect its own subtrees is the maximum of their heights. Update
maxTime = max(maxTime, abs(leftResult), abs(rightResult)). - Return
1to its parent, indicating thestartnode is at distance 1.
- The infection starts here. The time to infect its own subtrees is the maximum of their heights. Update
- If the
startnode was found in a child's subtree (e.g.,leftResult > 0):- The infection path to the other subtree (right) goes up to the current node and then down. The time for this path is
leftResult(up) +abs(rightResult)(down). - Update
maxTime = max(maxTime, leftResult + abs(rightResult)). - Return
leftResult + 1to propagate the distance tostartupwards. - Handle the symmetric case for the right subtree.
- The infection path to the other subtree (right) goes up to the current node and then down. The time for this path is
- If the
startnode is not in the current node's subtree (leftResult <= 0andrightResult <= 0):- The function's job is to report the height of this subtree. Return
-(1 + max(abs(leftResult), abs(rightResult))).
- The function's job is to report the height of this subtree. Return
- Recursively call
Walkthrough
We perform a post-order traversal. For any given node, after visiting its children, we have two key pieces of information: the result from the left subtree and the result from the right subtree. Based on these results and the current node's value, we can determine the maximum infection time involving this node and its subtrees.
The recursive function traverse(node, start) cleverly encodes its findings in its return value:
- Positive value
d: Thestartnode is in this subtree,dedges away from the current node. - Negative value
-h: Thestartnode is not in this subtree, and its height ish.
Let's analyze the logic at a node after recursive calls to its children:
- If
nodeis thestartnode: The infection spreads downwards. The time is the height of the taller child subtree. We update our globalmaxTimewith this value. We then return1to the parent, signaling that thestartnode is its immediate child. - If
startis in a subtree (e.g., left): We know the distance fromstartto the currentnode(which isleftResult). The longest path fromstartcould be one that goes up tonodeand then all the way down the right subtree. The time for this path isleftResult + height_of_right_subtree. We updatemaxTimewith this potential maximum. We then returnleftResult + 1to the parent, continuing to propagate the distance tostart. - If
startis not in the subtree: The node is just part of a regular branch. We calculate its height based on its children's heights and return it as a negative number to the parent.
This allows us to calculate both downward infection times (from start into its own subtree) and upward-then-downward infection times in a single traversal.
class Solution { private int maxTime = 0; public int amountOfTime(TreeNode root, int start) { traverse(root, start); return maxTime; } /** * Returns the distance from node to start if start is in the subtree. * Otherwise, returns the negative height of the subtree. */ private int traverse(TreeNode node, int start) { if (node == null) { return 0; } int leftResult = traverse(node.left, start); int rightResult = traverse(node.right, start); if (node.val == start) { // Infection starts here. Max time to infect its own subtrees. // The heights are the absolute values of the results from children. maxTime = Math.max(Math.abs(leftResult), Math.abs(rightResult)); // Return 1 to parent, indicating distance to start. return 1; } if (leftResult > 0 || rightResult > 0) { // Start node is in one of the subtrees. // The infection has to travel up to the current node and then down the other subtree. maxTime = Math.max(maxTime, Math.abs(leftResult) + Math.abs(rightResult)); // Propagate the distance to the start node upwards. return (leftResult > 0 ? leftResult : rightResult) + 1; } else { // Start node is not in this subtree. Return the height of this subtree as a negative value. return Math.min(leftResult, rightResult) - 1; } }}Complexity
Time
O(N), as the DFS traversal visits each node in the tree exactly once.
Space
O(H), where H is the height of the tree. This space is consumed by the recursion stack. For a balanced tree, this is O(log N), which is a significant improvement over O(N). In the worst case of a skewed tree, it becomes O(N).
Trade-offs
Pros
Highly efficient in terms of space, using only O(H) space for the recursion stack.
Solves the problem in a single pass over the tree.
Generally faster in practice due to less overhead from auxiliary data structures.
Cons
The logic is more complex and less intuitive than the graph-based approach.
Overloading the meaning of the return value (positive for distance, negative for height) can be subtle and error-prone.
Solutions
Solution
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { private Map < Integer , List < Integer >> g = new HashMap <>(); public int amountOfTime ( TreeNode root , int start ) { dfs ( root ); Deque < Integer > q = new ArrayDeque <>(); Set < Integer > vis = new HashSet <>(); q . offer ( start ); int ans = - 1 ; while (! q . isEmpty ()) { ++ ans ; for ( int n = q . size (); n > 0 ; -- n ) { int i = q . pollFirst (); vis . add ( i ); if ( g . containsKey ( i )) { for ( int j : g . get ( i )) { if (! vis . contains ( j )) { q . offer ( j ); } } } } } return ans ; } private void dfs ( TreeNode root ) { if ( root == null ) { return ; } if ( root . left != null ) { g . computeIfAbsent ( root . val , k -> new ArrayList <>()). add ( root . left . val ); g . computeIfAbsent ( root . left . val , k -> new ArrayList <>()). add ( root . val ); } if ( root . right != null ) { g . computeIfAbsent ( root . val , k -> new ArrayList <>()). add ( root . right . val ); g . computeIfAbsent ( root . right . val , k -> new ArrayList <>()). add ( root . val ); } dfs ( root . left ); dfs ( root . right ); } }/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */ class Solution { public: unordered_map < int , vector < int >> g ; int amountOfTime ( TreeNode * root , int start ) { dfs ( root ); queue < int > q { { start } }; unordered_set < int > vis ; int ans = - 1 ; while ( q . size ()) { ++ ans ; for ( int n = q . size (); n ; -- n ) { int i = q . front (); q . pop (); vis . insert ( i ); for ( int j : g [ i ]) { if ( ! vis . count ( j )) { q . push ( j ); } } } } return ans ; } void dfs ( TreeNode * root ) { if ( ! root ) return ; if ( root -> left ) { g [ root -> val ]. push_back ( root -> left -> val ); g [ root -> left -> val ]. push_back ( root -> val ); } if ( root -> right ) { g [ root -> val ]. push_back ( root -> right -> val ); g [ root -> right -> val ]. push_back ( root -> val ); } dfs ( root -> left ); dfs ( root -> right ); } };# Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution : def amountOfTime ( self , root : Optional [ TreeNode ], start : int ) -> int : def dfs ( root ): if root is None : return if root . left : g [ root . val ]. append ( root . left . val ) g [ root . left . val ]. append ( root . val ) if root . right : g [ root . val ]. append ( root . right . val ) g [ root . right . val ]. append ( root . val ) dfs ( root . left ) dfs ( root . right ) g = defaultdict ( list ) dfs ( root ) vis = set () q = deque ([ start ]) ans = - 1 while q : ans += 1 for _ in range ( len ( q )): i = q . popleft () vis . add ( i ) for j in g [ i ]: if j not in vis : q . append ( j ) return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.