3203. Find Minimum Diameter After Merging Two Trees
Problem Description
You are given two undirected trees with n
and m
nodes respectively. The first tree has nodes numbered from 0
to n-1
, and the second tree has nodes numbered from 0
to m-1
. The edges of these trees are provided in two arrays: edges1
(with n-1
edges) and edges2
(with m-1
edges).
Your task is to connect these two trees by adding exactly one edge between any node from the first tree and any node from the second tree. After connecting the trees, you need to find the minimum possible diameter of the resulting combined tree.
The diameter of a tree is defined as the length of the longest path between any two nodes in the tree (measured by the number of edges in the path).
For example:
-
If the first tree has edges
[[0,1],[0,2],[0,3]]
(a star with center 0) and the second tree has edges[[0,1]]
(just two connected nodes), connecting any node from the first tree to any node from the second tree will result in a combined tree with diameter 3. -
In another example with two identical larger trees, connecting node 0 from the first tree with node 0 from the second tree results in a diameter of 5.
The key insight is that to minimize the diameter of the merged tree, you need to consider:
- The original diameters
d1
andd2
of the two trees - The optimal connection point, which involves finding the "center" of each tree (the node that minimizes the maximum distance to any other node)
The final answer will be the maximum of:
- The original diameter of tree 1 (
d1
) - The original diameter of tree 2 (
d2
) - The new diameter created by connecting the two trees optimally:
⌈d1/2⌉ + ⌈d2/2⌉ + 1
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 involves two trees, which are special types of graphs. Trees are connected, acyclic graphs where we have nodes and edges.
Is it a tree?
- Yes: The problem explicitly states we are working with two undirected trees. We have
n
nodes withn-1
edges for the first tree andm
nodes withm-1
edges for the second tree, which is the defining characteristic of trees.
DFS
- Yes: Since we're dealing with trees, DFS is the natural choice for traversal.
Conclusion: The flowchart suggests using DFS (Depth-First Search) for this problem.
Why DFS is the Right Choice
The problem requires finding the diameter of trees, which involves:
- Finding the longest path between any two nodes in each tree
- Determining optimal connection points to minimize the diameter after merging
DFS is particularly well-suited for this because:
- Tree traversal: DFS efficiently explores all paths from a given node in a tree
- Distance calculation: We can track distances while traversing with DFS
- Finding farthest nodes: The two-pass DFS technique (first find the farthest node from any starting point, then find the farthest node from that node) is a classic algorithm for finding tree diameter
- No cycles: Since trees have no cycles, we don't need to worry about visiting nodes multiple times, making DFS implementation straightforward
The solution uses DFS twice for each tree to find their respective diameters, which is the standard approach for finding the longest path in a tree.
Intuition
When we connect two trees with a single edge, the diameter of the resulting tree depends on how we make that connection. Let's think about what happens:
The diameter of the merged tree could come from three possible scenarios:
- The longest path stays entirely within the first tree (original diameter
d1
) - The longest path stays entirely within the second tree (original diameter
d2
) - The longest path crosses through our new connecting edge
For scenario 3, we need to minimize the longest path that goes through the connection. The key insight is that we should connect nodes that are as "central" as possible in each tree.
Think of it this way: if we connect leaf nodes or nodes near the edge of each tree, we'd create very long paths. But if we connect nodes near the "center" of each tree, we minimize the maximum distance to any other node.
The "radius" of a tree is the minimum eccentricity (maximum distance from a node to any other node) among all nodes. For a tree with diameter d
, the radius is ⌈d/2⌉
. This represents the distance from the center to the farthest node.
When we connect the two trees at their optimal points (near their centers), the new diameter through the connection would be:
- Distance from farthest node in tree 1 to connection point:
⌈d1/2⌉
- The connecting edge itself:
1
- Distance from connection point to farthest node in tree 2:
⌈d2/2⌉
- Total:
⌈d1/2⌉ + ⌈d2/2⌉ + 1
To find the diameter of each tree efficiently, we use a clever two-pass DFS approach:
- First DFS: Start from any node and find the farthest node from it (call it node
a
) - Second DFS: Start from node
a
and find the farthest node from it (call it nodeb
) - The path from
a
tob
is guaranteed to be a diameter of the tree
This works because in a tree, one endpoint of a diameter must be the farthest node from any given starting point, and the other endpoint must be the farthest from that first endpoint.
Finally, we take the maximum of all three possible diameters: max(d1, d2, ⌈d1/2⌉ + ⌈d2/2⌉ + 1)
.
Learn more about Tree, Depth-First Search, Breadth-First Search and Graph patterns.
Solution Approach
The implementation consists of two main parts: finding the diameter of each tree and calculating the minimum diameter after merging.
Finding Tree Diameter with Two DFS Passes:
The treeDiameter
function implements the two-pass DFS algorithm:
-
Build the adjacency list: Convert the edge list into a graph representation using a dictionary where each node maps to its neighbors.
g = defaultdict(list) for a, b in edges: g[a].append(b) g[b].append(a)
-
First DFS pass: Start from node 0 and find the farthest node from it.
- The
dfs
function takes three parameters: current nodei
, parent nodefa
(to avoid revisiting), and current distancet
- We track the maximum distance
ans
and the corresponding farthest nodea
ans = a = 0 dfs(0, -1, 0) # Start from node 0 with no parent and distance 0
- The
-
Second DFS pass: Start from the farthest node
a
found in the first pass.dfs(a, -1, 0) # Start from node a to find the actual diameter
The maximum distance found in this pass is the diameter of the tree.
DFS Implementation Details:
The recursive DFS function explores all neighbors except the parent:
def dfs(i: int, fa: int, t: int):
for j in g[i]:
if j != fa: # Don't go back to parent
dfs(j, i, t + 1) # Increment distance by 1
nonlocal ans, a
if ans < t: # Update if we found a farther node
ans = t
a = i
Calculating Minimum Diameter After Merge:
Once we have both diameters d1
and d2
, we calculate the minimum possible diameter of the merged tree:
return max(d1, d2, (d1 + 1) // 2 + (d2 + 1) // 2 + 1)
This formula accounts for three cases:
d1
: The diameter remains within the first treed2
: The diameter remains within the second tree(d1 + 1) // 2 + (d2 + 1) // 2 + 1
: The diameter passes through the connecting edge
The expression (d1 + 1) // 2
computes ⌈d1/2⌉
(ceiling division) in Python, which gives us the radius of each tree. Adding 1 accounts for the connecting edge itself.
Time Complexity: O(n + m)
where n
and m
are the number of nodes in the two trees, as we perform DFS on each tree twice.
Space Complexity: O(n + m)
for storing the adjacency lists and the recursion stack during DFS.
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 illustrate the solution approach.
Example Input:
- Tree 1: edges1 =
[[0,1],[0,2]]
(3 nodes, shaped like a V with node 0 at center) - Tree 2: edges2 =
[[0,1],[1,2],[2,3]]
(4 nodes, a straight line)
Step 1: Find diameter of Tree 1
First, build adjacency list for Tree 1:
0: [1, 2] 1: [0] 2: [0]
First DFS from node 0:
- Visit 0 (distance 0)
- Visit 1 (distance 1)
- Visit 2 (distance 1)
- Farthest node is either 1 or 2 with distance 1 (let's say node 1)
Second DFS from node 1:
- Visit 1 (distance 0)
- Visit 0 (distance 1)
- Visit 2 (distance 2)
- Farthest node is 2 with distance 2
Diameter of Tree 1 (d1) = 2
Step 2: Find diameter of Tree 2
Build adjacency list for Tree 2:
0: [1] 1: [0, 2] 2: [1, 3] 3: [2]
First DFS from node 0:
- Visit 0 (distance 0)
- Visit 1 (distance 1)
- Visit 2 (distance 2)
- Visit 3 (distance 3)
- Farthest node is 3 with distance 3
Second DFS from node 3:
- Visit 3 (distance 0)
- Visit 2 (distance 1)
- Visit 1 (distance 2)
- Visit 0 (distance 3)
- Farthest node is 0 with distance 3
Diameter of Tree 2 (d2) = 3
Step 3: Calculate minimum diameter after merge
We need to find the maximum of:
- d1 = 2
- d2 = 3
- ⌈d1/2⌉ + ⌈d2/2⌉ + 1 = ⌈2/2⌉ + ⌈3/2⌉ + 1 = 1 + 2 + 1 = 4
Using integer arithmetic: (2+1)//2 + (3+1)//2 + 1 = 1 + 2 + 1 = 4
Result: max(2, 3, 4) = 4
The minimum possible diameter when connecting these two trees is 4. This occurs when we connect node 0 from Tree 1 (its center) with node 1 or 2 from Tree 2 (near its center). The longest path would be from node 2 in Tree 1, through node 0, across the connecting edge, through nodes 1/2 in Tree 2, to node 0 or 3 in Tree 2.
Solution Implementation
1from typing import List
2from collections import defaultdict
3
4class Solution:
5 def minimumDiameterAfterMerge(
6 self, edges1: List[List[int]], edges2: List[List[int]]
7 ) -> int:
8 """
9 Find the minimum possible diameter after connecting two trees with one edge.
10
11 The strategy is to connect the centers of both trees to minimize the resulting diameter.
12 The new diameter will be the maximum of:
13 1. Original diameter of tree 1
14 2. Original diameter of tree 2
15 3. Ceiling(d1/2) + ceiling(d2/2) + 1 (path through the connecting edge)
16 """
17 # Calculate diameters of both trees
18 diameter1 = self.treeDiameter(edges1)
19 diameter2 = self.treeDiameter(edges2)
20
21 # The minimum diameter after merge is the maximum of:
22 # - Original diameter of tree 1
23 # - Original diameter of tree 2
24 # - Path from center of tree 1 to center of tree 2
25 # (d + 1) // 2 gives us the radius (distance from center to farthest node)
26 return max(diameter1, diameter2, (diameter1 + 1) // 2 + (diameter2 + 1) // 2 + 1)
27
28 def treeDiameter(self, edges: List[List[int]]) -> int:
29 """
30 Calculate the diameter of a tree using two DFS traversals.
31 First DFS finds one end of the longest path.
32 Second DFS from that end finds the actual diameter.
33 """
34 # Build adjacency list representation of the tree
35 graph = defaultdict(list)
36 for node_a, node_b in edges:
37 graph[node_a].append(node_b)
38 graph[node_b].append(node_a)
39
40 # Variables to track the maximum distance and farthest node
41 self.max_distance = 0
42 self.farthest_node = 0
43
44 def dfs(current_node: int, parent: int, distance: int) -> None:
45 """
46 Depth-first search to find the farthest node from the starting point.
47
48 Args:
49 current_node: Current node being visited
50 parent: Parent node to avoid revisiting
51 distance: Distance from the starting node
52 """
53 # Explore all neighbors except the parent
54 for neighbor in graph[current_node]:
55 if neighbor != parent:
56 dfs(neighbor, current_node, distance + 1)
57
58 # Update the farthest node if current distance is greater
59 if self.max_distance < distance:
60 self.max_distance = distance
61 self.farthest_node = current_node
62
63 # First DFS: Find one end of the diameter starting from node 0
64 dfs(0, -1, 0)
65
66 # Reset max_distance for second DFS
67 self.max_distance = 0
68
69 # Second DFS: Find the actual diameter starting from the farthest node
70 dfs(self.farthest_node, -1, 0)
71
72 return self.max_distance
73
1class Solution {
2 private List<Integer>[] adjacencyList;
3 private int maxDistance;
4 private int farthestNode;
5
6 /**
7 * Finds the minimum possible diameter after merging two trees by connecting them with an edge.
8 * The strategy is to connect the centers of both trees to minimize the resulting diameter.
9 *
10 * @param edges1 Array of edges representing the first tree
11 * @param edges2 Array of edges representing the second tree
12 * @return The minimum diameter after merging the two trees
13 */
14 public int minimumDiameterAfterMerge(int[][] edges1, int[][] edges2) {
15 // Calculate the diameter of each tree
16 int diameter1 = treeDiameter(edges1);
17 int diameter2 = treeDiameter(edges2);
18
19 // The minimum diameter after merge is the maximum of:
20 // 1. The original diameter of tree1
21 // 2. The original diameter of tree2
22 // 3. The sum of radii of both trees plus 1 (for the connecting edge)
23 // Radius = ceil(diameter/2) = (diameter + 1) / 2
24 int mergedDiameter = (diameter1 + 1) / 2 + (diameter2 + 1) / 2 + 1;
25
26 return Math.max(Math.max(diameter1, diameter2), mergedDiameter);
27 }
28
29 /**
30 * Calculates the diameter of a tree using two DFS traversals.
31 * First DFS finds the farthest node from an arbitrary starting point.
32 * Second DFS from that farthest node finds the actual diameter.
33 *
34 * @param edges Array of edges representing the tree
35 * @return The diameter of the tree
36 */
37 public int treeDiameter(int[][] edges) {
38 int nodeCount = edges.length + 1;
39
40 // Initialize adjacency list for the tree
41 adjacencyList = new List[nodeCount];
42 Arrays.setAll(adjacencyList, index -> new ArrayList<>());
43
44 // Reset instance variables for diameter calculation
45 maxDistance = 0;
46 farthestNode = 0;
47
48 // Build the adjacency list from edges
49 for (int[] edge : edges) {
50 int nodeA = edge[0];
51 int nodeB = edge[1];
52 adjacencyList[nodeA].add(nodeB);
53 adjacencyList[nodeB].add(nodeA);
54 }
55
56 // First DFS: Find the farthest node from node 0
57 dfs(0, -1, 0);
58
59 // Second DFS: Find the farthest node from the previously found farthest node
60 // This gives us the tree diameter
61 dfs(farthestNode, -1, 0);
62
63 return maxDistance;
64 }
65
66 /**
67 * Depth-first search to find the farthest node and maximum distance from a starting node.
68 *
69 * @param currentNode The current node being visited
70 * @param parentNode The parent node to avoid revisiting
71 * @param currentDistance The distance from the starting node to the current node
72 */
73 private void dfs(int currentNode, int parentNode, int currentDistance) {
74 // Explore all neighbors except the parent
75 for (int neighbor : adjacencyList[currentNode]) {
76 if (neighbor != parentNode) {
77 dfs(neighbor, currentNode, currentDistance + 1);
78 }
79 }
80
81 // Update the maximum distance and farthest node if current distance is greater
82 if (maxDistance < currentDistance) {
83 maxDistance = currentDistance;
84 farthestNode = currentNode;
85 }
86 }
87}
88
1class Solution {
2public:
3 int minimumDiameterAfterMerge(vector<vector<int>>& edges1, vector<vector<int>>& edges2) {
4 // Calculate the diameter of both trees
5 int diameter1 = treeDiameter(edges1);
6 int diameter2 = treeDiameter(edges2);
7
8 // The minimum diameter after merging is the maximum of:
9 // 1. Original diameter of tree1
10 // 2. Original diameter of tree2
11 // 3. Ceiling of diameter1/2 + ceiling of diameter2/2 + 1 (edge connecting centers)
12 return max({diameter1, diameter2, (diameter1 + 1) / 2 + (diameter2 + 1) / 2 + 1});
13 }
14
15private:
16 int treeDiameter(vector<vector<int>>& edges) {
17 // Number of nodes in the tree (edges + 1 for a tree)
18 int numNodes = edges.size() + 1;
19
20 // Build adjacency list representation of the tree
21 vector<vector<int>> adjacencyList(numNodes);
22 for (const auto& edge : edges) {
23 int nodeA = edge[0];
24 int nodeB = edge[1];
25 adjacencyList[nodeA].push_back(nodeB);
26 adjacencyList[nodeB].push_back(nodeA);
27 }
28
29 // Variables to track the maximum distance and farthest node
30 int maxDistance = 0;
31 int farthestNode = 0;
32
33 // DFS lambda function to find the farthest node from a given starting node
34 auto dfs = [&](this auto&& dfs, int currentNode, int parentNode, int currentDistance) -> void {
35 // Explore all neighbors except the parent
36 for (int neighbor : adjacencyList[currentNode]) {
37 if (neighbor != parentNode) {
38 dfs(neighbor, currentNode, currentDistance + 1);
39 }
40 }
41
42 // Update the farthest node if current distance is greater
43 if (maxDistance < currentDistance) {
44 maxDistance = currentDistance;
45 farthestNode = currentNode;
46 }
47 };
48
49 // First DFS: Find the farthest node from an arbitrary starting node (node 0)
50 dfs(0, -1, 0);
51
52 // Second DFS: Find the farthest node from the previously found farthest node
53 // This gives us the diameter of the tree
54 maxDistance = 0;
55 dfs(farthestNode, -1, 0);
56
57 return maxDistance;
58 }
59};
60
1/**
2 * Calculates the minimum possible diameter after merging two trees by connecting them with an edge.
3 * The strategy is to connect the trees at points that minimize the resulting diameter.
4 *
5 * @param edges1 - Edge list representation of the first tree
6 * @param edges2 - Edge list representation of the second tree
7 * @returns The minimum diameter of the merged tree
8 */
9function minimumDiameterAfterMerge(edges1: number[][], edges2: number[][]): number {
10 // Calculate the diameter of each tree
11 const diameter1 = treeDiameter(edges1);
12 const diameter2 = treeDiameter(edges2);
13
14 // The minimum diameter after merging is the maximum of:
15 // 1. The original diameter of tree 1
16 // 2. The original diameter of tree 2
17 // 3. The sum of the radii of both trees plus the connecting edge
18 // (radius = ceil(diameter / 2))
19 return Math.max(
20 diameter1,
21 diameter2,
22 Math.ceil(diameter1 / 2) + Math.ceil(diameter2 / 2) + 1
23 );
24}
25
26/**
27 * Calculates the diameter of a tree (longest path between any two nodes).
28 * Uses two DFS traversals: first to find one endpoint of the diameter,
29 * then another from that endpoint to find the actual diameter.
30 *
31 * @param edges - Edge list representation of the tree
32 * @returns The diameter of the tree
33 */
34function treeDiameter(edges: number[][]): number {
35 // Number of nodes in the tree (edges + 1 for a tree)
36 const nodeCount = edges.length + 1;
37
38 // Build adjacency list representation of the tree
39 const adjacencyList: number[][] = Array.from({ length: nodeCount }, () => []);
40 for (const [nodeA, nodeB] of edges) {
41 adjacencyList[nodeA].push(nodeB);
42 adjacencyList[nodeB].push(nodeA);
43 }
44
45 // Variables to track the maximum distance and the farthest node
46 let maxDistance = 0;
47 let farthestNode = 0;
48
49 /**
50 * Depth-first search to find the farthest node from the starting node.
51 *
52 * @param currentNode - Current node being visited
53 * @param parentNode - Parent node to avoid revisiting
54 * @param currentDistance - Distance from the starting node
55 */
56 const dfs = (currentNode: number, parentNode: number, currentDistance: number): void => {
57 // Explore all neighbors except the parent
58 for (const neighbor of adjacencyList[currentNode]) {
59 if (neighbor !== parentNode) {
60 dfs(neighbor, currentNode, currentDistance + 1);
61 }
62 }
63
64 // Update the farthest node if current distance is greater
65 if (maxDistance < currentDistance) {
66 maxDistance = currentDistance;
67 farthestNode = currentNode;
68 }
69 };
70
71 // First DFS: Find one endpoint of the diameter starting from node 0
72 dfs(0, -1, 0);
73
74 // Second DFS: Find the actual diameter starting from the farthest node found
75 dfs(farthestNode, -1, 0);
76
77 return maxDistance;
78}
79
Time and Space Complexity
The time complexity of this algorithm is O(n + m)
, where n
is the number of nodes in the first tree and m
is the number of nodes in the second tree.
Time Complexity Analysis:
- The
treeDiameter
function is called twice, once for each tree - For the first tree with
n
nodes:- Building the adjacency list from edges takes
O(n-1) = O(n)
time (since a tree withn
nodes hasn-1
edges) - The DFS is performed twice from the function, each visiting all
n
nodes exactly once, takingO(n)
time per DFS - Total for first tree:
O(n) + O(n) + O(n) = O(n)
- Building the adjacency list from edges takes
- For the second tree with
m
nodes:- Similarly, this takes
O(m)
time
- Similarly, this takes
- The final
max
operation takesO(1)
time - Overall time complexity:
O(n) + O(m) = O(n + m)
Space Complexity Analysis:
The space complexity is O(n + m)
.
- For the first tree:
- The adjacency list
g
stores all edges, requiringO(n)
space - The recursive DFS call stack can go up to depth
O(n)
in the worst case (when the tree is a chain) - Space for first tree:
O(n)
- The adjacency list
- For the second tree:
- Similarly requires
O(m)
space
- Similarly requires
- The variables
ans
,a
,d1
,d2
useO(1)
space - Overall space complexity:
O(n) + O(m) = O(n + m)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Handling Single-Node Trees
One of the most common pitfalls is not properly handling edge cases where one or both trees consist of a single node (no edges). When edges
is empty, the tree has only one node, and its diameter should be 0.
Problem Code:
def treeDiameter(self, edges: List[List[int]]) -> int:
graph = defaultdict(list)
for node_a, node_b in edges:
graph[node_a].append(node_b)
graph[node_b].append(node_a)
# This will fail if edges is empty!
dfs(0, -1, 0) # Node 0 exists but has no neighbors
Solution: Add an early return check for empty edge lists:
def treeDiameter(self, edges: List[List[int]]) -> int:
# Handle single-node tree
if not edges:
return 0
graph = defaultdict(list)
# ... rest of the code
2. Integer Division vs Ceiling Division
Another pitfall is incorrectly calculating the ceiling division for the radius. Using regular division or floor division can give wrong results.
Problem Code:
# Wrong: This gives floor division
return max(d1, d2, d1 // 2 + d2 // 2 + 1)
# Also wrong: This doesn't handle odd diameters correctly
return max(d1, d2, math.ceil(d1 / 2) + math.ceil(d2 / 2) + 1)
Solution:
Use the formula (d + 1) // 2
which correctly computes ceiling division for non-negative integers:
return max(d1, d2, (d1 + 1) // 2 + (d2 + 1) // 2 + 1)
3. Using Instance Variables Instead of Closure Variables
Using self.max_distance
and self.farthest_node
as instance variables can cause issues in concurrent scenarios or when the method is called multiple times.
Problem Code:
class Solution:
def treeDiameter(self, edges):
self.max_distance = 0 # Instance variable - persists between calls
self.farthest_node = 0
# ... rest of code
Solution:
Use local variables with nonlocal
keyword or pass them as parameters:
def treeDiameter(self, edges):
max_distance = 0
farthest_node = 0
def dfs(current, parent, distance):
nonlocal max_distance, farthest_node
# ... rest of code
4. Incorrect Starting Node Assumption
Assuming node 0 always exists can fail when the tree nodes don't include 0.
Problem Code:
# Assumes node 0 exists dfs(0, -1, 0)
Solution: Start from any valid node in the graph:
if not graph:
return 0
# Start from any node in the graph
start_node = next(iter(graph))
dfs(start_node, -1, 0)
A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".
What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?
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!