Facebook Pixel

2359. Find Closest Node to Given Two Nodes

Problem Description

You have a directed graph with n nodes labeled from 0 to n - 1. Each node can have at most one outgoing edge (meaning each node points to at most one other node).

The graph is represented by an array edges of size n where:

  • edges[i] indicates there's a directed edge from node i to node edges[i]
  • If node i has no outgoing edge, then edges[i] == -1

Given two specific nodes node1 and node2, you need to find a node that can be reached from both node1 and node2. Among all such reachable nodes, you want to select the one that minimizes the maximum distance from either starting node.

More specifically:

  • For each node that can be reached from both node1 and node2, calculate the distance from node1 to that node and the distance from node2 to that node
  • Take the maximum of these two distances
  • Find the node where this maximum distance is as small as possible

If multiple nodes have the same minimal maximum distance, return the one with the smallest index. If no node can be reached from both node1 and node2, return -1.

Note that the graph may contain cycles (a path that leads back to a previously visited node).

Example: If node1 can reach node x with distance 3, and node2 can reach node x with distance 5, then the maximum distance for node x would be max(3, 5) = 5. You want to find the node where this maximum is minimized across all commonly reachable nodes.

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 a directed graph with n nodes numbered from 0 to n - 1, where each node has at most one outgoing edge represented by the edges array.

Is it a tree?

  • No: The problem states that the graph may contain cycles, and each node can have at most one outgoing edge (not necessarily forming a tree structure). A tree would have no cycles and exactly n-1 edges for n nodes.

Is the problem related to directed acyclic graphs?

  • No: The problem explicitly mentions that "edges may contain cycles", so this is not a DAG (Directed Acyclic Graph).

Is the problem related to shortest paths?

  • Yes: We need to find distances from node1 and node2 to all reachable nodes, then find the node that minimizes the maximum of these two distances. This is fundamentally a shortest path problem.

Is the graph weighted?

  • No: The edges don't have weights - each edge has an implicit weight of 1 (we're counting the number of edges as the distance).

BFS

  • Yes: Since we have an unweighted graph and need to find shortest paths, BFS is the appropriate choice.

Conclusion: The flowchart suggests using BFS for this problem. While the title mentions DFS pattern, the actual solution uses BFS because:

  1. We need to find shortest paths in an unweighted graph
  2. BFS guarantees the shortest path in terms of number of edges
  3. The solution code confirms this by using a deque and level-by-level traversal (q = deque([i]) and q.popleft())

The BFS approach calculates distances from both starting nodes to all reachable nodes, then finds the common node with the minimum maximum distance.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that we need to find a "meeting point" that both nodes can reach, and we want this meeting point to be as close as possible to both starting nodes simultaneously.

Think of it like two people starting from different locations trying to meet somewhere. They want to minimize the waiting time for whoever arrives later. The person who has to travel further determines the actual "cost" of that meeting point.

Since each node has at most one outgoing edge, following paths from any node is straightforward - we just keep following the single outgoing edge until we either hit a dead end (edges[i] == -1) or enter a cycle.

To solve this efficiently, we can:

  1. Calculate all distances from both starting points: Use BFS from node1 to find distances to all reachable nodes. Do the same from node2. BFS is perfect here because we're dealing with an unweighted graph where each edge has the same "cost" of 1.

  2. Find common reachable nodes: A node is a valid meeting point only if both node1 and node2 can reach it. This means the distance from both nodes must be finite (not infinity).

  3. Minimize the maximum distance: For each valid meeting point, the "cost" is determined by whichever node has to travel further to reach it. We want to minimize this cost across all possible meeting points.

The reason we use max(d1[i], d2[i]) is because both nodes travel simultaneously. If node1 takes 3 steps to reach point X and node2 takes 5 steps, they'll meet when the slower one arrives - after 5 steps. That's why we care about the maximum of the two distances.

Finally, if multiple nodes have the same optimal maximum distance, we choose the one with the smallest index as per the problem requirements.

Learn more about Depth-First Search and Graph patterns.

Solution Approach

The solution implements BFS to calculate distances from both starting nodes, then finds the optimal meeting point by examining all reachable nodes.

Step 1: Build the Graph Representation

g = defaultdict(list)
for i, j in enumerate(edges):
    if j != -1:
        g[i].append(j)

We convert the edges array into an adjacency list representation. Each node i points to node edges[i] if edges[i] != -1.

Step 2: BFS Distance Calculation Function

def f(i):
    dist = [inf] * n
    dist[i] = 0
    q = deque([i])
    while q:
        i = q.popleft()
        for j in g[i]:
            if dist[j] == inf:
                dist[j] = dist[i] + 1
                q.append(j)
    return dist

This helper function performs BFS from a given starting node:

  • Initialize all distances to infinity (inf), except the starting node which is 0
  • Use a queue to process nodes level by level
  • For each unvisited neighbor (distance still inf), update its distance and add it to the queue
  • Return the distance array containing shortest paths from the starting node to all reachable nodes

Step 3: Calculate Distances from Both Nodes

d1 = f(node1)
d2 = f(node2)

Run BFS from both node1 and node2 to get their respective distance arrays.

Step 4: Find the Optimal Meeting Point

ans, d = -1, inf
for i, (a, b) in enumerate(zip(d1, d2)):
    if (t := max(a, b)) < d:
        d = t
        ans = i
  • Iterate through all nodes using enumerate(zip(d1, d2)) to get both distances simultaneously
  • For each node i, calculate max(d1[i], d2[i]) - this is the "cost" of meeting at node i
  • Keep track of the node with the minimum cost
  • If a node has distance inf from either starting node, it won't be selected (since max(a, b) would be inf)

The time complexity is O(n) for building the graph plus O(n) for each BFS operation, resulting in overall O(n) complexity. The space complexity is O(n) for storing the graph and distance arrays.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

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

Given:

  • edges = [2, 2, 3, -1, 3]
  • node1 = 0
  • node2 = 1

This creates the following graph:

0 β†’ 2 β†’ 3 (no outgoing edge from 3)
1 β†’ 2
4 β†’ 3

Step 1: Build Graph Representation

g = {0: [2], 1: [2], 2: [3], 4: [3]}

Step 2: BFS from node1 = 0

  • Start: distance[0] = 0, queue = [0]
  • Process 0: Visit neighbor 2, distance[2] = 1, queue = [2]
  • Process 2: Visit neighbor 3, distance[3] = 2, queue = [3]
  • Process 3: No neighbors (edges[3] = -1)
  • Result: d1 = [0, inf, 1, 2, inf]

Node 0 can reach: itself (distance 0), node 2 (distance 1), and node 3 (distance 2).

Step 3: BFS from node2 = 1

  • Start: distance[1] = 0, queue = [1]
  • Process 1: Visit neighbor 2, distance[2] = 1, queue = [2]
  • Process 2: Visit neighbor 3, distance[3] = 2, queue = [3]
  • Process 3: No neighbors
  • Result: d2 = [inf, 0, 1, 2, inf]

Node 1 can reach: itself (distance 0), node 2 (distance 1), and node 3 (distance 2).

Step 4: Find Optimal Meeting Point Examine each node and calculate max(d1[i], d2[i]):

  • Node 0: max(0, inf) = inf (not reachable from node2)
  • Node 1: max(inf, 0) = inf (not reachable from node1)
  • Node 2: max(1, 1) = 1 βœ“
  • Node 3: max(2, 2) = 2 βœ“
  • Node 4: max(inf, inf) = inf (not reachable from either)

Common reachable nodes are 2 and 3 with maximum distances of 1 and 2 respectively.

Answer: Node 2 (minimum maximum distance = 1)

This demonstrates why we use the maximum distance: at node 2, both starting nodes need 1 step to meet, so they arrive simultaneously. At node 3, both need 2 steps. Node 2 is optimal because it minimizes the travel time for the slower traveler.

Solution Implementation

1from collections import defaultdict, deque
2from typing import List
3
4class Solution:
5    def closestMeetingNode(self, edges: List[int], node1: int, node2: int) -> int:
6        """
7        Find the node that minimizes the maximum distance from both node1 and node2.
8      
9        Args:
10            edges: List where edges[i] represents the outgoing edge from node i (-1 if no edge)
11            node1: Starting node 1
12            node2: Starting node 2
13          
14        Returns:
15            The index of the meeting node, or -1 if no common reachable node exists
16        """
17      
18        def compute_distances_from_node(start_node: int) -> List[float]:
19            """
20            Compute shortest distances from start_node to all other nodes using BFS.
21          
22            Args:
23                start_node: The node to start BFS from
24              
25            Returns:
26                List of distances where distances[i] is the distance from start_node to node i
27            """
28            # Initialize all distances as infinity
29            distances = [float('inf')] * num_nodes
30            distances[start_node] = 0
31          
32            # BFS queue initialized with the starting node
33            queue = deque([start_node])
34          
35            while queue:
36                current_node = queue.popleft()
37              
38                # Explore neighbors of current node
39                for neighbor in adjacency_list[current_node]:
40                    # If neighbor hasn't been visited yet
41                    if distances[neighbor] == float('inf'):
42                        distances[neighbor] = distances[current_node] + 1
43                        queue.append(neighbor)
44          
45            return distances
46      
47        # Build adjacency list from edges array
48        adjacency_list = defaultdict(list)
49        for source_node, target_node in enumerate(edges):
50            if target_node != -1:  # -1 indicates no outgoing edge
51                adjacency_list[source_node].append(target_node)
52      
53        # Total number of nodes in the graph
54        num_nodes = len(edges)
55      
56        # Compute distances from both starting nodes
57        distances_from_node1 = compute_distances_from_node(node1)
58        distances_from_node2 = compute_distances_from_node(node2)
59      
60        # Find the best meeting node
61        best_meeting_node = -1
62        min_max_distance = float('inf')
63      
64        # Check each node as a potential meeting point
65        for node_index, (dist_from_node1, dist_from_node2) in enumerate(zip(distances_from_node1, distances_from_node2)):
66            # Calculate the maximum distance for this meeting node
67            max_distance_to_node = max(dist_from_node1, dist_from_node2)
68          
69            # Update the best meeting node if this one is better
70            if max_distance_to_node < min_max_distance:
71                min_max_distance = max_distance_to_node
72                best_meeting_node = node_index
73      
74        return best_meeting_node
75
1class Solution {
2    private int numberOfNodes;
3    private List<Integer>[] adjacencyList;
4  
5    /**
6     * Finds the node that can be reached by both node1 and node2 with the minimum maximum distance.
7     * @param edges Array where edges[i] represents the outgoing edge from node i (-1 if no edge)
8     * @param node1 Starting node 1
9     * @param node2 Starting node 2
10     * @return The node index with minimum maximum distance, or -1 if no common reachable node exists
11     */
12    public int closestMeetingNode(int[] edges, int node1, int node2) {
13        numberOfNodes = edges.length;
14      
15        // Build adjacency list representation of the graph
16        adjacencyList = new List[numberOfNodes];
17        Arrays.setAll(adjacencyList, index -> new ArrayList<>());
18      
19        // Populate adjacency list based on edges array
20        for (int i = 0; i < numberOfNodes; i++) {
21            if (edges[i] != -1) {
22                adjacencyList[i].add(edges[i]);
23            }
24        }
25      
26        // Calculate distances from node1 to all other nodes
27        int[] distancesFromNode1 = calculateDistances(node1);
28      
29        // Calculate distances from node2 to all other nodes
30        int[] distancesFromNode2 = calculateDistances(node2);
31      
32        // Find the node with minimum maximum distance from both starting nodes
33        int minMaxDistance = 1 << 30;  // Initialize with a large value (2^30)
34        int resultNode = -1;
35      
36        for (int i = 0; i < numberOfNodes; i++) {
37            // Get the maximum distance to node i from either starting node
38            int maxDistanceToCurrentNode = Math.max(distancesFromNode1[i], distancesFromNode2[i]);
39          
40            // Update result if we found a better (smaller) maximum distance
41            if (maxDistanceToCurrentNode < minMaxDistance) {
42                minMaxDistance = maxDistanceToCurrentNode;
43                resultNode = i;
44            }
45        }
46      
47        return resultNode;
48    }
49  
50    /**
51     * Performs BFS to calculate shortest distances from a starting node to all other nodes.
52     * @param startNode The starting node for BFS
53     * @return Array of distances where distances[i] is the shortest distance from startNode to node i
54     */
55    private int[] calculateDistances(int startNode) {
56        // Initialize distances array with large values (unreachable)
57        int[] distances = new int[numberOfNodes];
58        Arrays.fill(distances, 1 << 30);  // Use 2^30 as infinity
59      
60        // Distance to starting node is 0
61        distances[startNode] = 0;
62      
63        // BFS queue
64        Deque<Integer> queue = new ArrayDeque<>();
65        queue.offer(startNode);
66      
67        // Perform BFS
68        while (!queue.isEmpty()) {
69            int currentNode = queue.poll();
70          
71            // Explore all neighbors of current node
72            for (int neighbor : adjacencyList[currentNode]) {
73                // If neighbor hasn't been visited yet
74                if (distances[neighbor] == 1 << 30) {
75                    distances[neighbor] = distances[currentNode] + 1;
76                    queue.offer(neighbor);
77                }
78            }
79        }
80      
81        return distances;
82    }
83}
84
1class Solution {
2public:
3    int closestMeetingNode(vector<int>& edges, int node1, int node2) {
4        int n = edges.size();
5      
6        // Build adjacency list representation of the graph
7        // Each node has at most one outgoing edge
8        vector<vector<int>> graph(n);
9        for (int i = 0; i < n; ++i) {
10            if (edges[i] != -1) {
11                graph[i].push_back(edges[i]);
12            }
13        }
14      
15        const int INF = 1 << 30;  // Large value representing infinity
16      
17        // Lambda function to compute shortest distances from a starting node using BFS
18        auto computeDistances = [&](int startNode) -> vector<int> {
19            vector<int> distances(n, INF);
20            distances[startNode] = 0;
21          
22            // BFS to find shortest paths
23            queue<int> bfsQueue;
24            bfsQueue.push(startNode);
25          
26            while (!bfsQueue.empty()) {
27                int currentNode = bfsQueue.front();
28                bfsQueue.pop();
29              
30                // Explore neighbors (at most one neighbor due to graph structure)
31                for (int neighbor : graph[currentNode]) {
32                    if (distances[neighbor] == INF) {  // Not visited yet
33                        distances[neighbor] = distances[currentNode] + 1;
34                        bfsQueue.push(neighbor);
35                    }
36                }
37            }
38          
39            return distances;
40        };
41      
42        // Calculate distances from node1 and node2 to all other nodes
43        vector<int> distancesFromNode1 = computeDistances(node1);
44        vector<int> distancesFromNode2 = computeDistances(node2);
45      
46        // Find the node with minimum maximum distance from both starting nodes
47        int resultNode = -1;
48        int minMaxDistance = INF;
49      
50        for (int i = 0; i < n; ++i) {
51            // Maximum distance from either node1 or node2 to node i
52            int maxDistanceToNode = max(distancesFromNode1[i], distancesFromNode2[i]);
53          
54            // Update result if we found a better meeting node
55            if (maxDistanceToNode < minMaxDistance) {
56                minMaxDistance = maxDistanceToNode;
57                resultNode = i;
58            }
59        }
60      
61        return resultNode;
62    }
63};
64
1/**
2 * Finds the node that can be reached from both node1 and node2 with minimum maximum distance
3 * @param edges - Array where edges[i] represents the outgoing edge from node i (-1 if no edge)
4 * @param node1 - Starting node 1
5 * @param node2 - Starting node 2
6 * @returns The index of the meeting node with minimum maximum distance, or -1 if no common node exists
7 */
8function closestMeetingNode(edges: number[], node1: number, node2: number): number {
9    const nodeCount = edges.length;
10  
11    // Build adjacency list representation of the graph
12    const adjacencyList: number[][] = Array.from({ length: nodeCount }, () => []);
13    for (let i = 0; i < nodeCount; ++i) {
14        if (edges[i] !== -1) {
15            adjacencyList[i].push(edges[i]);
16        }
17    }
18  
19    const INFINITY = 1 << 30;
20  
21    /**
22     * Performs BFS from a starting node to calculate distances to all other nodes
23     * @param startNode - The node to start BFS from
24     * @returns Array of distances from startNode to all other nodes
25     */
26    const calculateDistances = (startNode: number): number[] => {
27        const distances: number[] = new Array(nodeCount).fill(INFINITY);
28        distances[startNode] = 0;
29      
30        const queue: number[] = [startNode];
31      
32        while (queue.length > 0) {
33            const currentNode = queue.shift()!;
34          
35            // Explore all neighbors of current node
36            for (const neighbor of adjacencyList[currentNode]) {
37                if (distances[neighbor] === INFINITY) {
38                    distances[neighbor] = distances[currentNode] + 1;
39                    queue.push(neighbor);
40                }
41            }
42        }
43      
44        return distances;
45    };
46  
47    // Calculate distances from both starting nodes
48    const distancesFromNode1 = calculateDistances(node1);
49    const distancesFromNode2 = calculateDistances(node2);
50  
51    // Find the node with minimum maximum distance from both starting nodes
52    let resultNode = -1;
53    let minimumMaxDistance = INFINITY;
54  
55    for (let i = 0; i < nodeCount; ++i) {
56        const maxDistanceToNode = Math.max(distancesFromNode1[i], distancesFromNode2[i]);
57      
58        if (maxDistanceToNode < minimumMaxDistance) {
59            minimumMaxDistance = maxDistanceToNode;
60            resultNode = i;
61        }
62    }
63  
64    return resultNode;
65}
66

Time and Space Complexity

Time Complexity: O(n)

The algorithm consists of the following operations:

  • Building the graph from edges: O(n) where n is the number of nodes
  • Running BFS from node1: O(n) - each node is visited at most once, and each edge is traversed at most once. Since this is a directed graph where each node has at most one outgoing edge, there are at most n edges total
  • Running BFS from node2: O(n) - same reasoning as above
  • Finding the minimum maximum distance: O(n) - iterating through all nodes once

Total time complexity: O(n) + O(n) + O(n) + O(n) = O(n)

Space Complexity: O(n)

The space usage includes:

  • Graph representation g: O(n) - storing at most n edges
  • Distance array d1: O(n)
  • Distance array d2: O(n)
  • BFS queue: O(n) in the worst case when all nodes are reachable
  • Other variables: O(1)

Total space complexity: O(n) + O(n) + O(n) + O(n) = O(n)

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

Common Pitfalls

1. Not Handling Cycles Properly in Distance Calculation

The Pitfall: A common mistake is using DFS or a simple traversal without tracking visited nodes, which can lead to infinite loops when the graph contains cycles. Since each node has at most one outgoing edge, cycles are very likely to occur.

Why It Happens: Consider a graph where node 0 β†’ 1 β†’ 2 β†’ 0 (forming a cycle). Without proper visited tracking, the traversal would loop infinitely: 0 β†’ 1 β†’ 2 β†’ 0 β†’ 1 β†’ 2 β†’ ...

The Solution: The BFS approach with visited tracking (using distances[neighbor] == float('inf') as the check) naturally handles cycles:

if distances[neighbor] == float('inf'):  # Only process unvisited nodes
    distances[neighbor] = distances[current_node] + 1
    queue.append(neighbor)

Once a node is visited (distance is no longer infinity), it won't be added to the queue again, preventing infinite loops.

2. Incorrectly Identifying Unreachable Nodes

The Pitfall: Forgetting to check if a node is reachable from both starting nodes before considering it as a valid meeting point. Simply checking max(dist1, dist2) without verifying both distances are finite can lead to incorrect results.

Why It Happens: If node X is reachable from node1 (distance = 3) but not from node2 (distance = inf), then max(3, inf) = inf. Without proper handling, this might still be processed, potentially causing the algorithm to return -1 even when valid meeting nodes exist.

The Solution: The current implementation handles this correctly because:

max_distance_to_node = max(dist_from_node1, dist_from_node2)
if max_distance_to_node < min_max_distance:  # inf won't be less than any finite number

Any node unreachable from either start point will have max_distance = inf, which will never be less than a finite min_max_distance, effectively filtering out invalid nodes.

3. Using DFS Instead of BFS for Shortest Path

The Pitfall: Using DFS to find distances in this graph structure, which doesn't guarantee shortest paths.

Why It Happens: DFS explores one path completely before backtracking. In a graph with cycles or multiple paths, DFS might find a longer path to a node before finding the shortest one.

Example:

edges = [1, 2, 3, 1, -1]  
# Graph: 0β†’1β†’2β†’3β†’1 (cycle between 1,2,3)

DFS from node 0 might reach node 3 via path 0β†’1β†’2β†’3 (distance 3), while BFS correctly identifies it can be reached in distance 3.

The Solution: Always use BFS for unweighted shortest path problems:

queue = deque([start_node])
while queue:
    current_node = queue.popleft()  # Process nodes level by level

BFS explores all nodes at distance d before exploring nodes at distance d+1, guaranteeing shortest paths.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?


Recommended Readings

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

Load More