310. Minimum Height Trees
Problem Description
You're given a tree with n
nodes labeled from 0
to n - 1
. The tree is represented as an undirected graph where any two nodes are connected by exactly one path (no cycles exist).
The tree is defined by:
n
: the number of nodesedges
: an array ofn - 1
edges where eachedges[i] = [ai, bi]
represents an undirected edge between nodesai
andbi
Your task is to find which nodes, when chosen as the root, would create trees with minimum height. The height of a rooted tree is the number of edges on the longest path from the root down to any leaf node.
Key Points:
- You can choose any node as the root of the tree
- Different root choices result in trees of different heights
- You need to find all nodes that minimize this height
- Return all such nodes (there can be multiple valid answers)
Example Understanding:
If you have a linear chain of nodes 0-1-2-3-4
, choosing node 2
(the middle) as root gives height 2, while choosing node 0
(an end) as root gives height 4. The middle node(s) minimize the height.
Special Case:
- If
n = 1
, there's only one node, so return[0]
The solution uses a clever approach: it repeatedly removes leaf nodes (nodes with only one connection) layer by layer from the outside in. The last remaining nodes are the centers of the tree, which form the minimum height trees when used as roots. This works because the optimal roots are always in the "center" of the tree structure.
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 states we have an undirected graph representing a tree with
n
nodes andn-1
edges.
Is it a tree?
- Yes: The problem clearly defines the input as a tree - an undirected graph where any two vertices are connected by exactly one path (no cycles).
DFS
- We arrive at DFS as the algorithm choice for tree problems.
Conclusion: The flowchart suggests using DFS (Depth-First Search) for this tree problem.
Why DFS is Applicable
While the provided solution actually uses BFS with topological sorting (a level-by-level approach), DFS is equally valid for this problem:
-
Tree Traversal: DFS can be used to traverse the tree and calculate heights from any potential root node.
-
Height Calculation: For each node as a potential root, DFS can compute the maximum depth (height) of the tree by recursively exploring all paths.
-
Finding Centers: DFS can help identify the tree's center(s) by:
- Computing the eccentricity of each node (maximum distance to any other node)
- Finding nodes with minimum eccentricity
The solution uses a clever optimization with BFS and topological sorting to avoid repeated DFS calls, but the core problem of finding minimum height trees can definitely be solved using DFS by:
- Running DFS from each node to find its height when used as root
- Keeping track of the minimum height found
- Returning all nodes that achieve this minimum height
Intuition
Think about what makes a tree have minimum height when rooted at a particular node. The key insight is that the center of the tree will minimize the maximum distance to all other nodes.
Imagine a tree as a physical object - if you want to minimize the furthest distance from a central point to any edge, you'd place that point at the center. Similarly, the root nodes that create minimum height trees are those at the "center" of the graph structure.
But how do we find the center? Consider this observation: leaf nodes can never be optimal roots (except in trivial cases). Why? Because a leaf node is at the "edge" of the tree - choosing it as root means all other nodes are on one side, maximizing the height.
This leads to a brilliant approach: peel away the leaves layer by layer, like peeling an onion.
Here's the key reasoning:
- Start with all leaf nodes (nodes with degree 1)
- Remove them simultaneously, which exposes a new layer of leaf nodes
- Keep peeling until we can't peel anymore
- What remains? The center node(s) of the tree!
Why does this work? Because we're symmetrically removing the outermost nodes from all directions. The last nodes to be removed are equidistant from all the furthest leaf nodes - these are our centers.
An important property: a tree can have at most 2 center nodes. If it has more than 2 nodes at the center, it would create a cycle, violating the tree property.
The algorithm essentially finds the nodes that would be visited last in a "reverse" BFS that starts from all leaves simultaneously and moves inward. These central nodes, when chosen as roots, balance the tree optimally, minimizing the maximum distance to any leaf.
Solution Approach
The implementation uses topological sorting with BFS to systematically peel off leaf nodes layer by layer until we reach the center(s) of the tree.
Data Structures Used:
- Adjacency List (
g
): Store the graph representation whereg[i]
contains all neighbors of nodei
- Degree Array (
degree
): Track the number of connections for each node - Queue (
deque
): Store current layer of leaf nodes for BFS processing - Answer List (
ans
): Keep track of nodes in the current layer
Algorithm Steps:
-
Handle Edge Case:
if n == 1: return [0]
If there's only one node, it's automatically the root of the minimum height tree.
-
Build Graph Representation:
g = [[] for _ in range(n)] degree = [0] * n for a, b in edges: g[a].append(b) g[b].append(a) degree[a] += 1 degree[b] += 1
Create an adjacency list and count the degree (number of connections) for each node.
-
Initialize Queue with Leaf Nodes:
q = deque(i for i in range(n) if degree[i] == 1)
Find all nodes with degree 1 (leaf nodes) and add them to the queue.
-
Layer-by-Layer Peeling:
while q: ans.clear() for _ in range(len(q)): a = q.popleft() ans.append(a) for b in g[a]: degree[b] -= 1 if degree[b] == 1: q.append(b)
- Process all nodes in the current layer (current leaves)
- For each leaf, reduce the degree of its neighbors by 1
- If a neighbor becomes a new leaf (degree becomes 1), add it to the queue for the next layer
- Clear and update
ans
with each layer, soans
always contains the most recent layer processed
-
Return the Center Nodes: The last layer processed (remaining in
ans
) contains the center node(s) of the tree, which are the roots of minimum height trees.
Key Insight: By processing all leaves of each layer simultaneously, we ensure that we're moving inward symmetrically from all directions. The algorithm terminates when we've reached the center, leaving us with either 1 or 2 nodes (a tree can have at most 2 centers).
Time Complexity: O(n)
- We visit each node and edge once
Space Complexity: O(n)
- For storing the graph and auxiliary data structures
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 concrete example to see how the algorithm finds minimum height trees.
Example Input:
n = 6
edges = [[0,1], [0,2], [2,3], [2,4], [4,5]]
This creates the following tree structure:
1 | 0---2---4---5 | 3
Step 1: Build the graph and identify initial leaves
After building the adjacency list and calculating degrees:
- Node 0: neighbors = [1, 2], degree = 2
- Node 1: neighbors = [0], degree = 1 (leaf!)
- Node 2: neighbors = [0, 3, 4], degree = 3
- Node 3: neighbors = [2], degree = 1 (leaf!)
- Node 4: neighbors = [2, 5], degree = 2
- Node 5: neighbors = [4], degree = 1 (leaf!)
Initial queue: [1, 3, 5]
(all nodes with degree 1)
Step 2: First layer peeling
Process nodes 1, 3, and 5:
- Remove node 1: Reduce degree of node 0 from 2 to 1 β Node 0 becomes a new leaf!
- Remove node 3: Reduce degree of node 2 from 3 to 2
- Remove node 5: Reduce degree of node 4 from 2 to 1 β Node 4 becomes a new leaf!
After first layer:
ans = [1, 3, 5]
- New queue:
[0, 4]
- Remaining nodes with their updated degrees: {0:1, 2:2, 4:1}
Step 3: Second layer peeling
Process nodes 0 and 4:
- Remove node 0: Reduce degree of node 2 from 2 to 1 β Node 2 becomes a new leaf!
- Remove node 4: Reduce degree of node 2 from 1 to 0
After second layer:
ans = [0, 4]
- New queue:
[2]
- Remaining node: {2:0}
Step 4: Final layer
Process node 2:
- Remove node 2: No more neighbors to update
ans = [2]
- Queue becomes empty
Result: The algorithm returns [2]
as the only node that creates a minimum height tree.
Verification: Let's verify by checking the heights when different nodes are chosen as root:
- Root at node 0: Height = 3 (path: 0β2β4β5)
- Root at node 1: Height = 4 (path: 1β0β2β4β5)
- Root at node 2: Height = 2 (paths: 2β0β1, 2β4β5, 2β3 all have max length 2)
- Root at node 3: Height = 3 (path: 3β2β4β5)
- Root at node 4: Height = 3 (path: 4β2β0β1)
- Root at node 5: Height = 4 (path: 5β4β2β0β1)
Node 2 indeed produces the minimum height of 2, confirming our algorithm's result!
The key insight is that by peeling leaves symmetrically from all directions, we naturally converge on the center node(s) that balance the tree optimally.
Solution Implementation
1from typing import List
2from collections import deque
3
4class Solution:
5 def findMinHeightTrees(self, n: int, edges: List[List[int]]) -> List[int]:
6 # Handle edge case: single node tree
7 if n == 1:
8 return [0]
9
10 # Build adjacency list representation of the graph
11 adjacency_list = [[] for _ in range(n)]
12 node_degrees = [0] * n
13
14 # Populate adjacency list and count degrees for each node
15 for node_a, node_b in edges:
16 adjacency_list[node_a].append(node_b)
17 adjacency_list[node_b].append(node_a)
18 node_degrees[node_a] += 1
19 node_degrees[node_b] += 1
20
21 # Initialize queue with all leaf nodes (degree == 1)
22 leaves_queue = deque(node for node in range(n) if node_degrees[node] == 1)
23 remaining_nodes = []
24
25 # Iteratively remove leaf nodes layer by layer until we reach the center
26 while leaves_queue:
27 # Clear the result list for this iteration
28 remaining_nodes.clear()
29
30 # Process all leaves at the current level
31 current_level_size = len(leaves_queue)
32 for _ in range(current_level_size):
33 leaf_node = leaves_queue.popleft()
34 remaining_nodes.append(leaf_node)
35
36 # Update degrees of neighbors and add new leaves to queue
37 for neighbor in adjacency_list[leaf_node]:
38 node_degrees[neighbor] -= 1
39 if node_degrees[neighbor] == 1:
40 leaves_queue.append(neighbor)
41
42 # The last remaining nodes are the roots of minimum height trees
43 return remaining_nodes
44
1class Solution {
2 public List<Integer> findMinHeightTrees(int n, int[][] edges) {
3 // Special case: single node tree
4 if (n == 1) {
5 return List.of(0);
6 }
7
8 // Build adjacency list representation of the graph
9 List<Integer>[] adjacencyList = new List[n];
10 Arrays.setAll(adjacencyList, index -> new ArrayList<>());
11
12 // Track degree (number of connections) for each node
13 int[] nodeDegrees = new int[n];
14
15 // Populate adjacency list and degree array
16 for (int[] edge : edges) {
17 int nodeA = edge[0];
18 int nodeB = edge[1];
19 adjacencyList[nodeA].add(nodeB);
20 adjacencyList[nodeB].add(nodeA);
21 nodeDegrees[nodeA]++;
22 nodeDegrees[nodeB]++;
23 }
24
25 // Initialize queue with all leaf nodes (degree = 1)
26 Deque<Integer> leafQueue = new ArrayDeque<>();
27 for (int node = 0; node < n; ++node) {
28 if (nodeDegrees[node] == 1) {
29 leafQueue.offer(node);
30 }
31 }
32
33 // Result list to store minimum height tree roots
34 List<Integer> result = new ArrayList<>();
35
36 // Iteratively remove leaf nodes layer by layer
37 // The last remaining nodes are the centroids (MHT roots)
38 while (!leafQueue.isEmpty()) {
39 // Clear previous layer's nodes
40 result.clear();
41
42 // Process all nodes in current layer
43 int currentLayerSize = leafQueue.size();
44 for (int i = 0; i < currentLayerSize; ++i) {
45 int currentNode = leafQueue.poll();
46 result.add(currentNode);
47
48 // Update neighbors and add new leaves to queue
49 for (int neighbor : adjacencyList[currentNode]) {
50 nodeDegrees[neighbor]--;
51 if (nodeDegrees[neighbor] == 1) {
52 leafQueue.offer(neighbor);
53 }
54 }
55 }
56 }
57
58 return result;
59 }
60}
61
1class Solution {
2public:
3 vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
4 // Special case: single node tree
5 if (n == 1) {
6 return {0};
7 }
8
9 // Build adjacency list and track degree of each node
10 vector<vector<int>> adjacencyList(n);
11 vector<int> degrees(n);
12
13 for (const auto& edge : edges) {
14 int nodeA = edge[0];
15 int nodeB = edge[1];
16
17 // Add bidirectional edges
18 adjacencyList[nodeA].push_back(nodeB);
19 adjacencyList[nodeB].push_back(nodeA);
20
21 // Increment degree for both nodes
22 degrees[nodeA]++;
23 degrees[nodeB]++;
24 }
25
26 // Initialize queue with all leaf nodes (degree == 1)
27 queue<int> leafQueue;
28 for (int node = 0; node < n; ++node) {
29 if (degrees[node] == 1) {
30 leafQueue.push(node);
31 }
32 }
33
34 // Iteratively remove leaf nodes layer by layer
35 // The last remaining nodes are the roots of minimum height trees
36 vector<int> result;
37
38 while (!leafQueue.empty()) {
39 // Clear previous layer's results
40 result.clear();
41
42 // Process all nodes in current layer
43 int currentLayerSize = leafQueue.size();
44 for (int i = 0; i < currentLayerSize; ++i) {
45 int currentNode = leafQueue.front();
46 leafQueue.pop();
47
48 // Add current node to result (potential MHT root)
49 result.push_back(currentNode);
50
51 // Update neighbors and find new leaf nodes
52 for (int neighbor : adjacencyList[currentNode]) {
53 degrees[neighbor]--;
54
55 // If neighbor becomes a leaf after removing current node
56 if (degrees[neighbor] == 1) {
57 leafQueue.push(neighbor);
58 }
59 }
60 }
61 }
62
63 return result;
64 }
65};
66
1/**
2 * Finds all root nodes that minimize the height of the tree
3 * Uses a topological sort approach by repeatedly removing leaf nodes
4 * @param n - Number of nodes in the tree (0 to n-1)
5 * @param edges - Array of edges representing undirected connections
6 * @returns Array of node indices that form minimum height trees
7 */
8function findMinHeightTrees(n: number, edges: number[][]): number[] {
9 // Handle edge case: single node tree
10 if (n === 1) {
11 return [0];
12 }
13
14 // Build adjacency list representation of the graph
15 const adjacencyList: number[][] = Array.from({ length: n }, () => []);
16 const nodeDegrees: number[] = Array(n).fill(0);
17
18 // Populate adjacency list and calculate degrees for each node
19 for (const [nodeA, nodeB] of edges) {
20 adjacencyList[nodeA].push(nodeB);
21 adjacencyList[nodeB].push(nodeA);
22 nodeDegrees[nodeA]++;
23 nodeDegrees[nodeB]++;
24 }
25
26 // Initialize queue with all leaf nodes (degree = 1)
27 const leafQueue: number[] = [];
28 for (let nodeIndex = 0; nodeIndex < n; nodeIndex++) {
29 if (nodeDegrees[nodeIndex] === 1) {
30 leafQueue.push(nodeIndex);
31 }
32 }
33
34 // Process leaves layer by layer until we reach the center(s)
35 const result: number[] = [];
36 while (leafQueue.length > 0) {
37 // Clear previous result as we only want the final center nodes
38 result.length = 0;
39 const nextLayerLeaves: number[] = [];
40
41 // Process all current leaf nodes
42 for (const currentLeaf of leafQueue) {
43 result.push(currentLeaf);
44
45 // Update degrees of neighbors and identify new leaves
46 for (const neighbor of adjacencyList[currentLeaf]) {
47 nodeDegrees[neighbor]--;
48 if (nodeDegrees[neighbor] === 1) {
49 nextLayerLeaves.push(neighbor);
50 }
51 }
52 }
53
54 // Replace queue contents with next layer of leaves
55 leafQueue.splice(0, leafQueue.length, ...nextLayerLeaves);
56 }
57
58 return result;
59}
60
Time and Space Complexity
Time Complexity: O(n)
The algorithm processes each node and edge a constant number of times:
- Building the adjacency list takes
O(n)
time since we iterate through all edges once (there aren-1
edges in a tree) - Computing initial degrees takes
O(n)
time - The BFS-like traversal processes each node exactly once when it's removed from the queue, taking
O(n)
time total - For each node removal, we update its neighbors' degrees, which across all iterations touches each edge twice (once from each endpoint), resulting in
O(n)
operations total
Space Complexity: O(n)
The algorithm uses several data structures that scale with the number of nodes:
- The adjacency list
g
stores all edges, requiringO(n)
space (since a tree withn
nodes hasn-1
edges, and each edge is stored twice) - The
degree
array stores one value per node, usingO(n)
space - The queue
q
can contain at mostO(n)
nodes - The
ans
list stores at mostO(n)
nodes (though in practice it will be at most 2 for the final result)
Common Pitfalls
1. Not Handling the Single Node Case
Pitfall: Forgetting to handle n = 1
as a special case causes the algorithm to fail since there are no edges and no leaves with degree 1.
Issue Example:
# Without the edge case check
def findMinHeightTrees(self, n: int, edges: List[List[int]]) -> List[int]:
adjacency_list = [[] for _ in range(n)]
node_degrees = [0] * n
# When n=1 and edges=[], no nodes have degree 1
leaves_queue = deque(node for node in range(n) if node_degrees[node] == 1)
# Queue is empty, while loop never executes
# Returns empty list instead of [0]
Solution: Always check for n == 1
at the beginning and return [0]
immediately.
2. Incorrect Answer List Management
Pitfall: Not clearing the answer list at the start of each layer iteration, causing all processed nodes to accumulate instead of keeping only the last layer.
Issue Example:
remaining_nodes = []
while leaves_queue:
# Forgetting to clear - accumulates all nodes ever processed
# remaining_nodes.clear() # Missing this line!
for _ in range(len(leaves_queue)):
leaf_node = leaves_queue.popleft()
remaining_nodes.append(leaf_node)
# ...
# Returns all nodes instead of just the center nodes
Solution: Always call remaining_nodes.clear()
at the beginning of each while loop iteration to ensure only the last layer is retained.
3. Processing Queue Size Dynamically
Pitfall: Using len(leaves_queue)
directly in the loop condition instead of storing it first, causing incorrect layer processing.
Issue Example:
while leaves_queue:
remaining_nodes.clear()
# Wrong: queue size changes during iteration
for _ in range(len(leaves_queue)): # This evaluates each iteration!
leaf_node = leaves_queue.popleft()
for neighbor in adjacency_list[leaf_node]:
if node_degrees[neighbor] == 1:
leaves_queue.append(neighbor) # Queue grows during loop
Solution: Store the queue size before the loop:
current_level_size = len(leaves_queue)
for _ in range(current_level_size):
# Process exactly current_level_size nodes
4. Incorrect Degree Update Logic
Pitfall: Checking for new leaves before decrementing the degree, or using wrong threshold.
Issue Example:
for neighbor in adjacency_list[leaf_node]: # Wrong order - check before update if node_degrees[neighbor] == 2: # Wrong threshold leaves_queue.append(neighbor) node_degrees[neighbor] -= 1 # Should be before the check
Solution: Always decrement first, then check if degree becomes 1:
for neighbor in adjacency_list[leaf_node]: node_degrees[neighbor] -= 1 if node_degrees[neighbor] == 1: # Becomes a new leaf leaves_queue.append(neighbor)
5. Using Wrong Data Structure for Queue
Pitfall: Using a regular list with pop(0)
instead of deque
with popleft()
, resulting in O(n) time complexity for each removal.
Issue Example:
leaves_queue = [node for node in range(n) if node_degrees[node] == 1]
while leaves_queue:
leaf_node = leaves_queue.pop(0) # O(n) operation!
Solution: Use collections.deque
for O(1) operations:
from collections import deque
leaves_queue = deque(node for node in range(n) if node_degrees[node] == 1)
leaf_node = leaves_queue.popleft() # O(1) operation
Which data structure is used in a depth first search?
Recommended Readings
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
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Want a Structured Path to Master System Design Too? Donβt Miss This!