Facebook Pixel

3203. Find Minimum Diameter After Merging Two Trees


Problem Description

There exist two undirected trees with n and m nodes, numbered from 0 to n - 1 and from 0 to m - 1, respectively. You are given two 2D integer arrays edges1 and edges2 of lengths n - 1 and m - 1, respectively, where edges1[i] = [a_i, b_i] indicates that there is an edge between nodes a_i and b_i in the first tree and edges2[i] = [u_i, v_i] indicates that there is an edge between nodes u_i and v_i in the second tree.

You must connect one node from the first tree with another node from the second tree with an edge.

Return the minimum possible diameter of the resulting tree.

The diameter of a tree is the length of the longest path between any two nodes in the tree.

Intuition

To solve this problem, we need to understand how adding an edge between two trees affects their diameter. The key is to calculate the diameters of both trees individually and see how merging them could potentially reduce or keep the same diameter.

  1. Initial Diameters: Determine the diameter of each tree individually. The diameter of a tree is the longest path between any two nodes. We can efficiently find this using a two-pass Depth First Search (DFS):

    • Perform a DFS from any node to find the farthest node a.
    • From a, perform another DFS to find the farthest node b. The distance between a and b is the diameter of the tree.
  2. Edge Addition Impact: When we add an edge between two trees, we essentially connect the two trees' longest paths. The new diameter could either be one of the original diameters (if it's larger than the combination path) or be determined by the longest possible path that uses nodes from both trees.

  3. Calculate New Diameters:

    • Calculate r1 as the effective "radius" of the first tree: (d1 + 1) // 2.
    • Calculate r2 as the effective "radius" of the second tree: (d2 + 1) // 2.
    • The maximum diameter after merging can be max(d1, d2, r1 + r2 + 1). This accounts for the longest possible path that stretches across both trees plus the edge that connects them.

By adopting this approach, we can efficiently determine the minimum possible diameter after merging the two trees with a single edge.

Learn more about Tree, Depth-First Search, Breadth-First Search and Graph patterns.

Solution Approach

To implement the solution and find the minimum possible diameter after merging two trees, we employ the following steps and techniques:

  1. Tree Diameter Calculation:
    • We need to calculate the diameter for both trees separately. This is achieved through a method treeDiameter, which uses a Depth-First Search (DFS) strategy.
    • Start with an arbitrary node and perform a DFS from that node to find the farthest node, say a.
    • Conduct another DFS starting from a to find the farthest node from it, which we call b. The distance between a and b represents the tree's diameter.
def treeDiameter(self, edges: List[List[int]]) -> int:
    def dfs(i: int, fa: int, t: int):
        for j in g[i]:
            if j != fa:
                dfs(j, i, t + 1)
        nonlocal ans, a
        if ans < t:
            ans = t
            a = i

    g = defaultdict(list)
    for a, b in edges:
        g[a].append(b)
        g[b].append(a)
    ans = a = 0
    dfs(0, -1, 0)
    dfs(a, -1, 0)
    return ans
  1. Determine New Diameter after Merging:
    • Calculate d1 and d2 as the diameters of the first and second tree respectively.
    • Compute r1 and r2 as half the diameters, effectively representing the radius, rounded up: r1 = (d1 + 1) // 2 and r2 = (d2 + 1) // 2.
    • The potential new diameter after merging is calculated using max(d1, d2, r1 + r2 + 1). This checks:
      • If the existing diameters d1 or d2 are greater than the combined diameter via path stretching across the new connection (r1 + r2 + 1).
def minimumDiameterAfterMerge(self, edges1: List[List[int]], edges2: List[List[int]]) -> int:
    d1 = self.treeDiameter(edges1)
    d2 = self.treeDiameter(edges2)
    return max(d1, d2, (d1 + 1) // 2 + (d2 + 1) // 2 + 1)

By following these steps, the solution efficiently determines the minimum possible diameter after merging two trees by considering both individual diameters as well as the new longest path that can be created by connecting them.

Ready to land your dream job?

Unlock your dream job with a 2-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through an example to illustrate the solution approach.

Suppose we have two trees, the first tree with 4 nodes and the second tree with 3 nodes. The edges for these trees are given as follows:

  • Tree 1: edges1 = [[0, 1], [1, 2], [1, 3]]
  • Tree 2: edges2 = [[0, 1], [1, 2]]

Step 1: Calculate the Diameters of Both Trees Individually

  1. Tree 1 Diameter Calculation:

    • Start DFS from node 0. Traverse to find the farthest node, which becomes 2 or 3 with a distance of 2.
    • From node 2, perform another DFS to find the farthest node, which is 3 with a distance of 2. Thus, the diameter d1 = 2.
  2. Tree 2 Diameter Calculation:

    • Start DFS from node 0. Traverse to find the farthest node, which is 2 with a distance of 2.
    • From node 2, perform another DFS to find the farthest node, which is 0 with a distance of 2. Hence, the diameter d2 = 2.

Step 2: Calculate Radii of Both Trees

  • For Tree 1, calculate the radius: r1 = (d1 + 1) // 2 = (2 + 1) // 2 = 1
  • For Tree 2, calculate the radius: r2 = (d2 + 1) // 2 = (2 + 1) // 2 = 1

Step 3: Determine the Minimum Diameter After Merging

  • Compute the potential new diameter after merging: max(d1, d2, r1 + r2 + 1) = max(2, 2, 1 + 1 + 1) = 3

The minimum possible diameter of the resulting tree after connecting any node from Tree 1 to any node from Tree 2 is 3.

By following these steps, the approach demonstrates how to efficiently find the minimum possible diameter after merging two trees with one connecting edge.

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        # Calculate the diameter of each tree
9        diameter1 = self.treeDiameter(edges1)
10        diameter2 = self.treeDiameter(edges2)
11      
12        # The minimum diameter after merging is the maximum of the two diameters
13        # or the sum of half of each diameter plus one.
14        return max(diameter1, diameter2, (diameter1 + 1) // 2 + (diameter2 + 1) // 2 + 1)
15
16    def treeDiameter(self, edges: List[List[int]]) -> int:
17        # Depth-first search to find the farthest node from the starting node.
18        def dfs(node: int, parent: int, depth: int):
19            for neighbor in graph[node]:
20                if neighbor != parent:
21                    dfs(neighbor, node, depth + 1)
22          
23            nonlocal max_depth, farthest_node
24            if max_depth < depth:
25                max_depth = depth
26                farthest_node = node
27
28        # Build the adjacency list representation of the graph.
29        graph = defaultdict(list)
30        for node1, node2 in edges:
31            graph[node1].append(node2)
32            graph[node2].append(node1)
33
34        # Initialize variables for the maximum depth and farthest node.
35        max_depth = farthest_node = 0
36        # First DFS to find one endpoint of the diameter
37        dfs(0, -1, 0)
38        # Second DFS from the farthest node found to determine the diameter
39        dfs(farthest_node, -1, 0)
40      
41        return max_depth
42
1import java.util.ArrayList;
2import java.util.Arrays;
3import java.util.List;
4
5class Solution {
6    // Graph adjacency list
7    private List<Integer>[] graph;
8    // Maximum diameter found
9    private int maxDiameter;
10    // Furthest node found
11    private int furthestNode;
12
13    /**
14     * Calculates the minimum diameter after merging two trees.
15     * 
16     * @param edges1 The edges of the first tree.
17     * @param edges2 The edges of the second tree.
18     * @return The minimum diameter after merging the trees.
19     */
20    public int minimumDiameterAfterMerge(int[][] edges1, int[][] edges2) {
21        int diameter1 = treeDiameter(edges1);
22        int diameter2 = treeDiameter(edges2);
23      
24        // Calculate the effective diameter after potential merging
25        int mergedDiameter = (diameter1 + 1) / 2 + (diameter2 + 1) / 2 + 1;
26      
27        // Return the maximum diameter
28        return Math.max(Math.max(diameter1, diameter2), mergedDiameter);
29    }
30
31    /**
32     * Finds the diameter of a tree.
33     * 
34     * @param edges The edges representing the tree.
35     * @return The diameter of the tree.
36     */
37    public int treeDiameter(int[][] edges) {
38        int n = edges.length + 1; // Number of nodes
39
40        // Initialize graph as an adjacency list
41        graph = new List[n];
42        Arrays.setAll(graph, k -> new ArrayList<>());
43
44        // Reset maximum diameter and furthest node
45        maxDiameter = 0;
46        furthestNode = 0;
47
48        // Construct the graph
49        for (int[] edge : edges) {
50            int node1 = edge[0];
51            int node2 = edge[1];
52            graph[node1].add(node2);
53            graph[node2].add(node1);
54        }
55
56        // Perform DFS to find the furthest node from an arbitrary start (here, node 0)
57        dfs(0, -1, 0);
58        // Perform DFS again from the furthest node found to determine the tree's diameter
59        dfs(furthestNode, -1, 0);
60
61        // Return the maximum diameter found
62        return maxDiameter;
63    }
64
65    /**
66     * Depth-First Search to compute the furthest distance from the current node.
67     * 
68     * @param currentNode The current node in DFS.
69     * @param parent The parent node in DFS.
70     * @param currentLength The distance from the starting node.
71     */
72    private void dfs(int currentNode, int parent, int currentLength) {
73        for (int neighbor : graph[currentNode]) {
74            if (neighbor != parent) {
75                dfs(neighbor, currentNode, currentLength + 1);
76            }
77        }
78
79        // Update the maximum distance and furthest node if a longer path is found
80        if (maxDiameter < currentLength) {
81            maxDiameter = currentLength;
82            furthestNode = currentNode;
83        }
84    }
85}
86
1#include <vector>
2#include <algorithm>
3using namespace std;
4
5class Solution {
6public:
7    int minimumDiameterAfterMerge(vector<vector<int>>& edges1, vector<vector<int>>& edges2) {
8        // Calculate the diameter of each tree
9        int d1 = treeDiameter(edges1);
10        int d2 = treeDiameter(edges2);
11      
12        // Determine the minimum possible diameter after merging the two trees
13        // The diameter of the merged tree is the maximum of the diameters of the two original trees, 
14        // or the sum of half the diameters of each tree (rounded up) plus 1.
15        return max({d1, d2, (d1 + 1) / 2 + (d2 + 1) / 2 + 1});
16    }
17
18    int treeDiameter(vector<vector<int>>& edges) {
19        int n = edges.size() + 1; // Number of nodes
20
21        // Create adjacency list for the tree
22        vector<vector<int>> graph(n);
23        for (const auto& e : edges) {
24            int a = e[0], b = e[1];
25            graph[a].push_back(b);
26            graph[b].push_back(a);
27        }
28
29        int diameter = 0, farthestNode = 0;
30
31        // Depth-First Search (DFS) to calculate the diameter
32        // Lambda function used for DFS traversal
33        auto dfs = [&](auto&& dfs, int node, int parent, int depth) -> void {
34            for (int neighbor : graph[node]) {
35                if (neighbor != parent) {
36                    dfs(dfs, neighbor, node, depth + 1);
37                }
38            }
39            // Update farthest distance and node
40            if (diameter < depth) {
41                diameter = depth;
42                farthestNode = node;
43            }
44        };
45
46        // Execute DFS twice to find the tree diameter
47        dfs(dfs, 0, -1, 0);  // First DFS to find the farthest node from root
48        dfs(dfs, farthestNode, -1, 0); // Second DFS from farthest node to find the actual diameter
49      
50        return diameter;
51    }
52};
53
1function minimumDiameterAfterMerge(edges1: number[][], edges2: number[][]): number {
2    // Calculate the diameter of the first tree.
3    const d1 = treeDiameter(edges1);
4    // Calculate the diameter of the second tree.
5    const d2 = treeDiameter(edges2);
6
7    // Return the maximum of the two diameters or the minimum possible diameter after merging.
8    return Math.max(d1, d2, Math.ceil(d1 / 2) + Math.ceil(d2 / 2) + 1);
9}
10
11function treeDiameter(edges: number[][]): number {
12    // Number of nodes is one more than the number of edges in a tree.
13    const n = edges.length + 1;
14  
15    // Initialize adjacency list for the graph.
16    const graph: number[][] = Array.from({ length: n }, () => []);
17  
18    // Construct the graph.
19    for (const [a, b] of edges) {
20        graph[a].push(b);
21        graph[b].push(a);
22    }
23  
24    // Variables to store the maximum distance found and the corresponding node.
25    let [maxDistance, farthestNode] = [0, 0];
26  
27    // Depth-first search helper function.
28    const dfs = (currentNode: number, parent: number, distance: number): void => {
29        // Traverse each connected node.
30        for (const neighbor of graph[currentNode]) {
31            // Continue traversal only if the node is not the parent.
32            if (neighbor !== parent) {
33                dfs(neighbor, currentNode, distance + 1);
34            }
35        }
36        // Update the maximum distance and farthest node if current distance is greater.
37        if (maxDistance < distance) {
38            maxDistance = distance;
39            farthestNode = currentNode;
40        }
41    };
42  
43    // Perform a DFS to find a farthest node from node 0.
44    dfs(0, -1, 0);
45    // Perform another DFS from the farthest node found to determine the tree's diameter.
46    dfs(farthestNode, -1, 0);
47    return maxDistance;
48}
49

Time and Space Complexity

The minimumDiameterAfterMerge function processes two separate tree structures represented by edges1 and edges2. For each tree, it calculates the diameter using the treeDiameter function.

  • Time Complexity: Each call to treeDiameter involves two depth-first search (DFS) passes. Constructing the adjacency list takes O(n) time for the edges, and each DFS pass also takes O(n), where n is the number of nodes in a tree. Since treeDiameter is invoked twice, the overall time complexity for minimumDiameterAfterMerge is O(n + m) where n and m are the number of edges in edges1 and edges2, respectively.

  • Space Complexity: The space required is mainly due to the adjacency list and the recursion stack. The adjacency list requires O(n) space for storing edges, and the recursion stack in DFS can also take up to O(n) space for each tree. Therefore, the total space complexity is O(n + m), accounting for the two trees.

Learn more about how to find time and space complexity quickly.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

Which of these properties could exist for a graph but not a tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!


Load More