Problem of The Day: Find Minimum Diameter After Merging Two Trees
Problem Statement
Brute Force [TLE]
class Solution:
def minimumDiameterAfterMerge(self, edges1: List[List[int]], edges2: List[List[int]]) -> int:
graph1 = defaultdict(list)
graph2 = defaultdict(list)
set1 = set()
set2 = set()
res = 0
def create_graph(edges, graph, unique_nodes):
for a, b in edges:
graph[a].append(b)
graph[b].append(a)
unique_nodes.add(a)
unique_nodes.add(b)
def bfs(start, edges, graph, num_of_nodes):
distances = [-1] * num_of_nodes
queue = deque([start])
distances[start] = 0
while queue:
node = queue.popleft()
for nei in graph[node]:
if distances[nei] == -1:
distances[nei] = distances[node] + 1
queue.append(nei)
return distances[:start] + distances[start + 1:]
def find_min_diameter_from(unique_nodes, edges, graph):
nonlocal res
distance_from_node = []
for node in unique_nodes:
distances = bfs(node, edges, graph, len(unique_nodes))
distance_from_node.append(max(distances))
curr_max = max(distance_from_node) if distance_from_node else 0
res = max(res, curr_max)
return min(distance_from_node) if distance_from_node else 0
create_graph(edges1, graph1, set1)
create_graph(edges2, graph2, set2)
d1 = find_min_diameter_from(set1, edges1, graph1)
d2 = find_min_diameter_from(set2, edges2, graph2)
res = max(res, d1 + d2 + 1)
return res
Editorial
Approach 1: Farthest of Farthest (BFS)
class Solution:
def minimumDiameterAfterMerge(self, edges1, edges2):
# Calculate the number of nodes for each tree
n = len(edges1) + 1
m = len(edges2) + 1
# Build adjacency lists for both trees
adj_list1 = self.build_adj_list(n, edges1)
adj_list2 = self.build_adj_list(m, edges2)
# Calculate the diameters of both trees
diameter1 = self.find_diameter(n, adj_list1)
diameter2 = self.find_diameter(m, adj_list2)
# Calculate the longest path that spans across both trees
combined_diameter = ceil(diameter1 / 2) + ceil(diameter2 / 2) + 1
# Return the maximum of the three possibilities
return max(diameter1, diameter2, combined_diameter)
def build_adj_list(self, size, edges):
adj_list = [[] for _ in range(size)]
for edge in edges:
adj_list[edge[0]].append(edge[1])
adj_list[edge[1]].append(edge[0])
return adj_list
def find_diameter(self, n, adj_list):
# First BFS to find the farthest node from an arbitrary node (e.g., 0)
farthest_node, _ = self.find_farthest_node(n, adj_list, 0)
# Second BFS to find the diameter starting from the farthest node
_, diameter = self.find_farthest_node(n, adj_list, farthest_node)
return diameter
def find_farthest_node(self, n, adj_list, source_node):
queue = deque([source_node])
visited = [False] * n
visited[source_node] = True
maximum_distance = 0
farthest_node = source_node
while queue:
for _ in range(len(queue)):
current_node = queue.popleft()
farthest_node = current_node
for neighbor in adj_list[current_node]:
if not visited[neighbor]:
visited[neighbor] = True
queue.append(neighbor)
if queue:
maximum_distance += 1
return farthest_node, maximum_distance
Approach 2: Depth First Search
class Solution:
def minimumDiameterAfterMerge(
self, edges1: list[list[int]], edges2: list[list[int]]
) -> int:
# Calculate the number of nodes for each tree (number of edges + 1)
n = len(edges1) + 1
m = len(edges2) + 1
# Build adjacency lists for both trees
adj_list1 = self.build_adj_list(n, edges1)
adj_list2 = self.build_adj_list(m, edges2)
# Calculate the diameter of both trees
diameter1, _ = self.find_diameter(
adj_list1, 0, -1
) # Start DFS for Tree 1
diameter2, _ = self.find_diameter(
adj_list2, 0, -1
) # Start DFS for Tree 2
# Calculate the diameter of the combined tree
# This accounts for the longest path spanning both trees
combined_diameter = ceil(diameter1 / 2) + ceil(diameter2 / 2) + 1
# Return the maximum diameter among the two trees and the combined tree
return max(diameter1, diameter2, combined_diameter)
# Helper function to build an adjacency list from an edge list
def build_adj_list(
self, size: int, edges: list[list[int]]
) -> list[list[int]]:
adj_list = [[] for _ in range(size)]
for edge in edges:
adj_list[edge[0]].append(edge[1])
adj_list[edge[1]].append(edge[0])
return adj_list
# Helper function to find the diameter of a tree
# Returns the diameter and the depth of the node's subtree
def find_diameter(
self, adj_list: list[list[int]], node: int, parent: int
) -> tuple[int, int]:
max_depth1 = max_depth2 = (
0 # Tracks the two largest depths from the current node
)
diameter = 0 # Tracks the maximum diameter of the subtree
for neighbor in adj_list[node]:
if neighbor == parent:
continue # Skip the parent to avoid cycles
# Recursively calculate the diameter and depth of the neighbor's subtree
child_diameter, depth = self.find_diameter(adj_list, neighbor, node)
depth += 1 # Increment depth to include edge to neighbor
# Update the maximum diameter of the subtree
diameter = max(diameter, child_diameter)
# Update the two largest depths from the current node
if depth > max_depth1:
max_depth2 = max_depth1
max_depth1 = depth
elif depth > max_depth2:
max_depth2 = depth
# Update the diameter to include the path through the current node
diameter = max(diameter, max_depth1 + max_depth2)
# Return the diameter and the longest depth
return diameter, max_depth1
Approach 3: Topological Sorting
class Solution:
def minimumDiameterAfterMerge(self, edges1, edges2):
# Calculate the number of nodes for each tree (number of edges + 1)
n = len(edges1) + 1
m = len(edges2) + 1
# Build adjacency lists for both trees
adj_list1 = self.build_adj_list(n, edges1)
adj_list2 = self.build_adj_list(m, edges2)
# Calculate the diameter of both trees
diameter1 = self.find_diameter(n, adj_list1)
diameter2 = self.find_diameter(m, adj_list2)
# Calculate the longest path that spans across both trees
combined_diameter = ceil(diameter1 / 2) + ceil(diameter2 / 2) + 1
# Return the maximum of the three possibilities
return max(diameter1, diameter2, combined_diameter)
# Function to build an adjacency list from an edge list
def build_adj_list(self, size, edges):
adj_list = [[] for _ in range(size)]
for edge in edges:
adj_list[edge[0]].append(edge[1])
adj_list[edge[1]].append(edge[0])
return adj_list
# Function to find the diameter of a tree
def find_diameter(self, n, adj_list):
leaves_queue = deque()
degrees = [0] * n
# Initialize the degree of each node and add leaves (nodes with degree 1) to the queue
for node in range(n):
degrees[node] = len(adj_list[node])
if degrees[node] == 1:
leaves_queue.append(node)
remaining_nodes = n
leaves_layers_removed = 0
# Process the leaves until there are 2 or fewer nodes remaining
while remaining_nodes > 2:
size = len(leaves_queue)
remaining_nodes -= size
leaves_layers_removed += 1
# Remove the leaves from the queue and update the degrees of their neighbors
for _ in range(size):
current_node = leaves_queue.popleft()
# Process the neighbors of the current leaf
for neighbor in adj_list[current_node]:
degrees[neighbor] -= 1
if degrees[neighbor] == 1:
leaves_queue.append(neighbor)
# If exactly two nodes remain, return the diameter as twice the number of layers of leaves removed + 1
if remaining_nodes == 2:
return 2 * leaves_layers_removed + 1
return 2 * leaves_layers_removed