228 lines
10 KiB
YAML
228 lines
10 KiB
YAML
title: Amount of Time for Binary Tree to Be Infected
|
|
slug: amount-of-time-for-binary-tree-to-be-infected
|
|
difficulty: medium
|
|
leetcode_id: 2385
|
|
leetcode_url: https://leetcode.com/problems/amount-of-time-for-binary-tree-to-be-infected/
|
|
categories:
|
|
- trees
|
|
- graphs
|
|
- hash-tables
|
|
patterns:
|
|
- bfs
|
|
- tree-traversal
|
|
|
|
description: |
|
|
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*.
|
|
|
|
**Note:** In a binary tree, a node is adjacent to its parent and its children. The infection spreads in all directions — not just downward.
|
|
|
|
constraints: |
|
|
- `1 <= number of nodes <= 10^5`
|
|
- `1 <= Node.val <= 10^5`
|
|
- Each node has a **unique** value.
|
|
- A node with value `start` exists in the tree.
|
|
|
|
examples:
|
|
- input: "root = [1,5,3,null,4,10,6,9,2], start = 3"
|
|
output: "4"
|
|
explanation: "Starting from node 3, the infection spreads: Minute 0: Node 3. Minute 1: Nodes 1, 10, 6. Minute 2: Node 5. Minute 3: Node 4. Minute 4: Nodes 9, 2. Total time: 4 minutes."
|
|
- input: "root = [1], start = 1"
|
|
output: "0"
|
|
explanation: "The only node in the tree is the starting node, so it takes 0 minutes."
|
|
|
|
explanation:
|
|
intuition: |
|
|
At first glance, this looks like a tree traversal problem. But there's a twist: infection spreads **in all directions** — to children *and* to the parent. In a standard binary tree, we only have pointers going downward. How do we "go up"?
|
|
|
|
Think of it like this: imagine the tree as a network of rooms connected by hallways. Once a room catches fire, it spreads to all connected rooms — regardless of whether they're "above" or "below" in our original tree structure.
|
|
|
|
The key insight is to **convert the tree into an undirected graph**. Once we have a graph where each node knows all its neighbors (parent and children alike), we can use **Breadth-First Search (BFS)** starting from the infected node. BFS naturally explores nodes level by level — and each "level" corresponds to one minute of infection spread.
|
|
|
|
The answer is simply the **maximum distance** from the start node to any other node in this graph, which BFS finds efficiently.
|
|
|
|
approach: |
|
|
We solve this in two phases: first convert the tree to a graph, then run BFS.
|
|
|
|
**Step 1: Build an adjacency list (graph) from the tree**
|
|
|
|
- Use DFS to traverse the tree
|
|
- For each node, add bidirectional edges: parent ↔ child
|
|
- Store this in a hash map where each node value maps to a list of neighbor values
|
|
|
|
|
|
|
|
**Step 2: Run BFS from the start node**
|
|
|
|
- Initialize a queue with the `start` node and a `visited` set
|
|
- Track the current "minute" (distance from start)
|
|
- Process nodes level by level:
|
|
- For each node at the current level, add all unvisited neighbors to the next level
|
|
- Increment the minute counter after processing each level
|
|
|
|
|
|
|
|
**Step 3: Return the total time**
|
|
|
|
- When the queue is empty, all nodes are infected
|
|
- Return the number of minutes elapsed (which equals the maximum distance from start)
|
|
|
|
|
|
|
|
This approach works because BFS explores nodes in order of their distance from the source. The last level we process contains the farthest nodes, and the time to reach them is our answer.
|
|
|
|
common_pitfalls:
|
|
- title: Treating It as a Standard Tree Problem
|
|
description: |
|
|
A common mistake is trying to solve this with standard tree traversal, only considering paths going downward.
|
|
|
|
For example, if `start = 3` and node 3 is deep in the tree, the infection needs to spread **upward** to the root and then down other branches. Standard tree traversal doesn't give you a way to go from child to parent.
|
|
|
|
The fix is to convert the tree into an undirected graph where parent-child relationships become bidirectional edges.
|
|
wrong_approach: "DFS/BFS only going to children"
|
|
correct_approach: "Build undirected graph, then BFS from start"
|
|
|
|
- title: Using DFS Instead of BFS for Distance
|
|
description: |
|
|
DFS can find *a* path to every node, but it doesn't naturally give you the *shortest* path (minimum time). You'd need to track depths and handle backtracking carefully.
|
|
|
|
BFS is the right choice here because it explores nodes level by level. Each level corresponds to one minute, so when BFS completes, you've automatically found the maximum time needed.
|
|
wrong_approach: "DFS with manual depth tracking"
|
|
correct_approach: "BFS for level-order (shortest path) traversal"
|
|
|
|
- title: Forgetting to Track Visited Nodes
|
|
description: |
|
|
Since we're working with an undirected graph (bidirectional edges), if you don't track visited nodes, you'll revisit the same node repeatedly and loop forever.
|
|
|
|
For example: node A connects to B, B connects back to A. Without a visited set, your BFS will bounce between A and B infinitely.
|
|
wrong_approach: "BFS without visited set"
|
|
correct_approach: "Mark nodes as visited when adding to queue"
|
|
|
|
key_takeaways:
|
|
- "**Tree to Graph conversion**: When a tree problem requires bidirectional traversal (going to parent), convert it to an undirected graph first"
|
|
- "**BFS for shortest paths**: BFS naturally finds shortest paths in unweighted graphs — each level is one step further from the source"
|
|
- "**Level-order = time steps**: When modeling time-based spread (infection, fire, etc.), BFS levels correspond directly to time units"
|
|
- "**Related problems**: This pattern appears in problems like *Rotting Oranges*, *Walls and Gates*, and *Shortest Path in Binary Matrix*"
|
|
|
|
time_complexity: "O(n). We visit each node twice — once during graph construction (DFS) and once during BFS."
|
|
space_complexity: "O(n). We store the adjacency list (O(n) edges in a tree) plus the BFS queue and visited set (O(n) each)."
|
|
|
|
solutions:
|
|
- approach_name: Graph Conversion + BFS
|
|
is_optimal: true
|
|
code: |
|
|
from collections import defaultdict, deque
|
|
from typing import Optional
|
|
|
|
class TreeNode:
|
|
def __init__(self, val=0, left=None, right=None):
|
|
self.val = val
|
|
self.left = left
|
|
self.right = right
|
|
|
|
def amount_of_time(root: Optional[TreeNode], start: int) -> int:
|
|
# Step 1: Build adjacency list (undirected graph)
|
|
graph = defaultdict(list)
|
|
|
|
def build_graph(node: TreeNode, parent: Optional[TreeNode]) -> None:
|
|
if not node:
|
|
return
|
|
# Add bidirectional edge between node and parent
|
|
if parent:
|
|
graph[node.val].append(parent.val)
|
|
graph[parent.val].append(node.val)
|
|
# Recurse to children
|
|
build_graph(node.left, node)
|
|
build_graph(node.right, node)
|
|
|
|
build_graph(root, None)
|
|
|
|
# Step 2: BFS from start node
|
|
queue = deque([start])
|
|
visited = {start}
|
|
minutes = -1 # Start at -1 because we count levels, not nodes
|
|
|
|
while queue:
|
|
minutes += 1
|
|
# Process all nodes at current level (current minute)
|
|
for _ in range(len(queue)):
|
|
node = queue.popleft()
|
|
# Add all unvisited neighbors to next level
|
|
for neighbor in graph[node]:
|
|
if neighbor not in visited:
|
|
visited.add(neighbor)
|
|
queue.append(neighbor)
|
|
|
|
return minutes
|
|
explanation: |
|
|
**Time Complexity:** O(n) — We traverse all nodes twice: once for graph construction, once for BFS.
|
|
|
|
**Space Complexity:** O(n) — The adjacency list stores O(n) edges (a tree with n nodes has n-1 edges), and BFS uses O(n) for the queue and visited set.
|
|
|
|
The key insight is converting the tree to an undirected graph so we can traverse "upward" to parents. Then BFS gives us the maximum distance (time) naturally by exploring level by level.
|
|
|
|
- approach_name: One-Pass DFS with Distance Tracking
|
|
is_optimal: true
|
|
code: |
|
|
from typing import Optional
|
|
|
|
class TreeNode:
|
|
def __init__(self, val=0, left=None, right=None):
|
|
self.val = val
|
|
self.left = left
|
|
self.right = right
|
|
|
|
def amount_of_time(root: Optional[TreeNode], start: int) -> int:
|
|
max_time = 0
|
|
|
|
def dfs(node: Optional[TreeNode]) -> int:
|
|
"""
|
|
Returns the depth of the start node if found in this subtree,
|
|
or -1 if not found. Negative depths indicate distance above start.
|
|
"""
|
|
nonlocal max_time
|
|
|
|
if not node:
|
|
return -1
|
|
|
|
left_depth = dfs(node.left)
|
|
right_depth = dfs(node.right)
|
|
|
|
if node.val == start:
|
|
# Found start node - max time is depth of deepest child
|
|
max_time = max(max_time, max(left_depth, right_depth) + 1)
|
|
return 0 # Distance from start to itself is 0
|
|
|
|
if left_depth >= 0:
|
|
# Start is in left subtree
|
|
# Time to infect right subtree = distance to start + right depth + 1
|
|
max_time = max(max_time, left_depth + right_depth + 2)
|
|
return left_depth + 1 # Return distance to start
|
|
|
|
if right_depth >= 0:
|
|
# Start is in right subtree
|
|
max_time = max(max_time, left_depth + right_depth + 2)
|
|
return right_depth + 1
|
|
|
|
# Start not in this subtree - return max depth for potential use by ancestor
|
|
return max(left_depth, right_depth) + 1
|
|
|
|
# Handle edge case: single node tree
|
|
if not root.left and not root.right:
|
|
return 0
|
|
|
|
dfs(root)
|
|
return max_time
|
|
explanation: |
|
|
**Time Complexity:** O(n) — Single DFS traversal visiting each node once.
|
|
|
|
**Space Complexity:** O(h) — Only the recursion stack, where h is the tree height. O(log n) for balanced trees, O(n) worst case for skewed trees.
|
|
|
|
This approach avoids building an explicit graph. Instead, during DFS, we track whether the start node is in the current subtree and calculate distances on the fly. When we find a node that's an ancestor of start, we can compute the time to infect the "other" subtree.
|