Facebook Pixel

3112. Minimum Time to Visit Disappearing Nodes

Problem Description

You have an undirected graph with n nodes (numbered from 0 to n-1). The graph is described by a 2D array edges, where each element edges[i] = [ui, vi, lengthi] represents an edge between nodes ui and vi that takes lengthi units of time to traverse.

There's a time constraint: each node i disappears from the graph at time disappear[i]. Once a node disappears, you cannot visit it anymore.

Starting from node 0, you need to find the minimum time required to reach each node in the graph. If you reach a node at exactly the time it disappears or later, you cannot visit that node.

The graph may have these characteristics:

  • It might be disconnected (not all nodes are reachable from node 0)
  • It might contain multiple edges between the same pair of nodes

Your task is to return an array answer where:

  • answer[i] is the minimum time needed to reach node i from node 0
  • answer[i] is -1 if node i cannot be reached from node 0 (either because there's no path or because the node disappears before you can reach it)

For example, if the shortest path to node i takes 10 units of time but disappear[i] = 10, then node i is unreachable because it disappears at the exact moment you would arrive.

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

Intuition

This problem is asking for the shortest path from a source node (node 0) to all other nodes, which immediately suggests using a shortest path algorithm. The classic choice for this is Dijkstra's algorithm.

However, there's a twist - nodes have a "disappearance time." This means we need to consider not just the shortest path, but also whether we can reach a node before it disappears. If we arrive at a node at time t and the node disappears at time disappear[i], we can only visit the node if t < disappear[i].

The key insight is that we can modify Dijkstra's algorithm to handle this constraint. During the relaxation step, when we consider updating the distance to a neighbor node v through node u, we need to check two conditions:

  1. The new distance dist[u] + w is shorter than the current known distance dist[v]
  2. We can reach node v before it disappears: dist[u] + w < disappear[v]

Only when both conditions are met should we update the distance and continue exploring from that node.

Why does Dijkstra's algorithm work here? Because:

  • We're dealing with non-negative edge weights (time cannot be negative)
  • We want the minimum time to reach each node
  • The disappearance constraint doesn't change the fundamental property that once we've found the shortest path to a node, we don't need to revisit it

The algorithm naturally handles disconnected components - nodes that cannot be reached from node 0 will retain their initial infinite distance. Similarly, nodes that disappear before we can reach them won't have their distances updated, also remaining at infinity. In the final step, we convert these infinite distances to -1 as required by the problem.

Learn more about Graph, Shortest Path and Heap (Priority Queue) patterns.

Solution Approach

The solution implements a heap-optimized version of Dijkstra's algorithm with modifications to handle node disappearance times.

Step 1: Build the Graph

First, we construct an adjacency list representation of the graph using a defaultdict(list). For each edge [u, v, w], we add the connection in both directions since the graph is undirected:

  • Add (v, w) to g[u]
  • Add (u, w) to g[v]

Step 2: Initialize Distance Array

We create a distance array dist of size n, initialized with infinity (inf) for all nodes except the source node. Set dist[0] = 0 since the distance from node 0 to itself is 0.

Step 3: Priority Queue Setup

Initialize a min-heap priority queue pq with the tuple (0, 0), representing (distance, node). The heap ensures we always process the node with the smallest distance first.

Step 4: Modified Dijkstra's Algorithm

While the priority queue is not empty:

  1. Extract minimum: Pop the node u with the smallest distance du from the heap using heappop(pq).

  2. Skip outdated entries: If du > dist[u], this entry is outdated (we've already found a shorter path to u), so skip it and continue.

  3. Relax edges: For each neighbor v of node u with edge weight w:

    • Calculate the new potential distance: new_dist = dist[u] + w
    • Check two conditions:
      • Is this path shorter? dist[v] > new_dist
      • Can we reach v before it disappears? new_dist < disappear[v]
    • If both conditions are satisfied:
      • Update dist[v] = new_dist
      • Push (dist[v], v) to the priority queue

Step 5: Format the Result

After processing all reachable nodes, construct the final answer array. For each node i:

  • If dist[i] < disappear[i], the node is reachable before it disappears, so answer[i] = dist[i]
  • Otherwise, the node is unreachable, so answer[i] = -1

The list comprehension [a if a < b else -1 for a, b in zip(dist, disappear)] elegantly handles this conversion, where nodes with infinite distance or those that disappear before we can reach them are marked as -1.

Time Complexity: O((E + V) log V) where E is the number of edges and V is the number of vertices, due to the heap operations.

Space Complexity: O(V + E) for storing the graph and distance array.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

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

Example Input:

  • n = 4 (nodes: 0, 1, 2, 3)
  • edges = [[0,1,2], [0,2,4], [1,2,1], [2,3,3]]
  • disappear = [10, 5, 8, 1]

Step 1: Build the Graph

g[0] = [(1,2), (2,4)]
g[1] = [(0,2), (2,1)]
g[2] = [(0,4), (1,1), (3,3)]
g[3] = [(2,3)]

Step 2: Initialize Distance Array

dist = [0, inf, inf, inf]

Step 3: Priority Queue Setup

pq = [(0, 0)]  # (distance, node)

Step 4: Modified Dijkstra's Algorithm

Iteration 1:

  • Pop (0, 0) from pq
  • Process node 0 with distance 0
  • Check neighbors:
    • Node 1: new_dist = 0 + 2 = 2
      • 2 < inf? ✓
      • 2 < disappear[1]=5? ✓
      • Update: dist[1] = 2, push (2, 1) to pq
    • Node 2: new_dist = 0 + 4 = 4
      • 4 < inf? ✓
      • 4 < disappear[2]=8? ✓
      • Update: dist[2] = 4, push (4, 2) to pq
  • State: dist = [0, 2, 4, inf], pq = [(2,1), (4,2)]

Iteration 2:

  • Pop (2, 1) from pq
  • Process node 1 with distance 2
  • Check neighbors:
    • Node 0: new_dist = 2 + 2 = 4 (skip, 4 > dist[0]=0)
    • Node 2: new_dist = 2 + 1 = 3
      • 3 < 4? ✓
      • 3 < disappear[2]=8? ✓
      • Update: dist[2] = 3, push (3, 2) to pq
  • State: dist = [0, 2, 3, inf], pq = [(3,2), (4,2)]

Iteration 3:

  • Pop (3, 2) from pq
  • Process node 2 with distance 3
  • Check neighbors:
    • Node 0: new_dist = 3 + 4 = 7 (skip, 7 > dist[0]=0)
    • Node 1: new_dist = 3 + 1 = 4 (skip, 4 > dist[1]=2)
    • Node 3: new_dist = 3 + 3 = 6
      • 6 < inf? ✓
      • 6 < disappear[3]=1? ✗ (Node 3 disappears at time 1!)
      • Do not update
  • State: dist = [0, 2, 3, inf], pq = [(4,2)]

Iteration 4:

  • Pop (4, 2) from pq
  • This is outdated (4 > dist[2]=3), skip

Step 5: Format the Result

dist = [0, 2, 3, inf]
disappear = [10, 5, 8, 1]
  • Node 0: 0 < 10 ✓ → answer[0] = 0
  • Node 1: 2 < 5 ✓ → answer[1] = 2
  • Node 2: 3 < 8 ✓ → answer[2] = 3
  • Node 3: inf (unreachable) → answer[3] = -1

Final Answer: [0, 2, 3, -1]

Node 3 is unreachable because even though there's a path (0→2→3) that would take 6 time units, node 3 disappears at time 1, making it impossible to reach.

Solution Implementation

1from collections import defaultdict
2from heapq import heappush, heappop
3from typing import List
4from math import inf
5
6
7class Solution:
8    def minimumTime(
9        self, n: int, edges: List[List[int]], disappear: List[int]
10    ) -> List[int]:
11        # Build adjacency list representation of the graph
12        # Each node maps to a list of (neighbor, weight) tuples
13        graph = defaultdict(list)
14        for u, v, weight in edges:
15            graph[u].append((v, weight))
16            graph[v].append((u, weight))  # Undirected graph
17      
18        # Initialize distances array with infinity for all nodes
19        # distances[i] represents the shortest time to reach node i from node 0
20        distances = [inf] * n
21        distances[0] = 0  # Starting node has distance 0
22      
23        # Priority queue for Dijkstra's algorithm
24        # Each element is (distance, node)
25        priority_queue = [(0, 0)]
26      
27        # Dijkstra's algorithm with disappearing nodes constraint
28        while priority_queue:
29            current_distance, current_node = heappop(priority_queue)
30          
31            # Skip if we've already found a better path to this node
32            if current_distance > distances[current_node]:
33                continue
34          
35            # Explore all neighbors of the current node
36            for neighbor, edge_weight in graph[current_node]:
37                # Calculate the new distance to the neighbor
38                new_distance = distances[current_node] + edge_weight
39              
40                # Update distance only if:
41                # 1. The new path is shorter than the current known path
42                # 2. We can reach the neighbor before it disappears
43                if distances[neighbor] > new_distance and new_distance < disappear[neighbor]:
44                    distances[neighbor] = new_distance
45                    heappush(priority_queue, (new_distance, neighbor))
46      
47        # Convert distances to result format
48        # Return -1 for unreachable nodes (distance >= disappear time)
49        result = []
50        for distance, disappear_time in zip(distances, disappear):
51            if distance < disappear_time:
52                result.append(distance)
53            else:
54                result.append(-1)
55      
56        return result
57
1class Solution {
2    public int[] minimumTime(int n, int[][] edges, int[] disappear) {
3        // Build adjacency list representation of the graph
4        List<int[]>[] graph = new List[n];
5        Arrays.setAll(graph, index -> new ArrayList<>());
6      
7        // Add edges to the graph (undirected, so add both directions)
8        for (int[] edge : edges) {
9            int from = edge[0];
10            int to = edge[1];
11            int weight = edge[2];
12            graph[from].add(new int[] {to, weight});
13            graph[to].add(new int[] {from, weight});
14        }
15      
16        // Initialize distances array with maximum values
17        int[] distances = new int[n];
18        Arrays.fill(distances, 1 << 30); // Large value representing infinity
19        distances[0] = 0; // Starting node has distance 0
20      
21        // Priority queue for Dijkstra's algorithm: stores [distance, node]
22        PriorityQueue<int[]> priorityQueue = new PriorityQueue<>((a, b) -> a[0] - b[0]);
23        priorityQueue.offer(new int[] {0, 0}); // Start with node 0 at distance 0
24      
25        // Dijkstra's algorithm with disappearing nodes constraint
26        while (!priorityQueue.isEmpty()) {
27            int[] current = priorityQueue.poll();
28            int currentDistance = current[0];
29            int currentNode = current[1];
30          
31            // Skip if we've already found a better path to this node
32            if (currentDistance > distances[currentNode]) {
33                continue;
34            }
35          
36            // Explore neighbors
37            for (int[] neighbor : graph[currentNode]) {
38                int nextNode = neighbor[0];
39                int edgeWeight = neighbor[1];
40                int newDistance = distances[currentNode] + edgeWeight;
41              
42                // Update distance if we found a shorter path that arrives before node disappears
43                if (distances[nextNode] > newDistance && newDistance < disappear[nextNode]) {
44                    distances[nextNode] = newDistance;
45                    priorityQueue.offer(new int[] {distances[nextNode], nextNode});
46                }
47            }
48        }
49      
50        // Prepare result array: return distance if reachable before disappearing, otherwise -1
51        int[] result = new int[n];
52        for (int i = 0; i < n; i++) {
53            result[i] = distances[i] < disappear[i] ? distances[i] : -1;
54        }
55      
56        return result;
57    }
58}
59
1class Solution {
2public:
3    vector<int> minimumTime(int n, vector<vector<int>>& edges, vector<int>& disappear) {
4        // Build adjacency list representation of the graph
5        // Each node stores pairs of (neighbor_node, edge_weight)
6        vector<vector<pair<int, int>>> adjacencyList(n);
7        for (const auto& edge : edges) {
8            int nodeU = edge[0];
9            int nodeV = edge[1];
10            int weight = edge[2];
11          
12            // Add bidirectional edges
13            adjacencyList[nodeU].push_back({nodeV, weight});
14            adjacencyList[nodeV].push_back({nodeU, weight});
15        }
16
17        // Initialize distances with a large value (representing infinity)
18        const int INF = 1 << 30;
19        vector<int> distances(n, INF);
20        distances[0] = 0;  // Starting node has distance 0
21
22        // Min-heap priority queue: stores pairs of (distance, node)
23        // Ordered by distance (smallest first)
24        using NodeDistPair = pair<int, int>;
25        priority_queue<NodeDistPair, vector<NodeDistPair>, greater<NodeDistPair>> minHeap;
26        minHeap.push({0, 0});  // Start from node 0 with distance 0
27
28        // Dijkstra's algorithm with node disappearance constraint
29        while (!minHeap.empty()) {
30            // Extract node with minimum distance
31            auto [currentDist, currentNode] = minHeap.top();
32            minHeap.pop();
33
34            // Skip if we've already found a better path to this node
35            if (currentDist > distances[currentNode]) {
36                continue;
37            }
38
39            // Explore all neighbors of current node
40            for (auto [neighbor, edgeWeight] : adjacencyList[currentNode]) {
41                int newDistance = distances[currentNode] + edgeWeight;
42              
43                // Update distance if:
44                // 1. New path is shorter than current known distance
45                // 2. We can reach the neighbor before it disappears
46                if (distances[neighbor] > newDistance && newDistance < disappear[neighbor]) {
47                    distances[neighbor] = newDistance;
48                    minHeap.push({newDistance, neighbor});
49                }
50            }
51        }
52
53        // Prepare result: return distance if reachable before disappearance, otherwise -1
54        vector<int> result(n);
55        for (int i = 0; i < n; ++i) {
56            result[i] = (distances[i] < disappear[i]) ? distances[i] : -1;
57        }
58
59        return result;
60    }
61};
62
1/**
2 * Finds the minimum time to reach each node from node 0, considering nodes disappear at certain times
3 * @param n - Number of nodes in the graph
4 * @param edges - Array of edges where each edge is [u, v, weight]
5 * @param disappear - Array where disappear[i] is the time when node i disappears
6 * @returns Array of minimum distances, -1 if node is unreachable before it disappears
7 */
8function minimumTime(n: number, edges: number[][], disappear: number[]): number[] {
9    // Build adjacency list representation of the graph
10    // Each node maps to an array of [neighbor, weight] pairs
11    const adjacencyList: [number, number][][] = Array.from({ length: n }, () => []);
12  
13    // Add edges to adjacency list (undirected graph)
14    for (const [nodeU, nodeV, weight] of edges) {
15        adjacencyList[nodeU].push([nodeV, weight]);
16        adjacencyList[nodeV].push([nodeU, weight]);
17    }
18  
19    // Initialize distances array with infinity for all nodes
20    const distances = Array.from({ length: n }, () => Infinity);
21    distances[0] = 0; // Starting node has distance 0
22  
23    // Priority queue to process nodes by shortest distance
24    // Stores [distance, node] pairs, sorted by distance first, then by node
25    const priorityQueue = new PriorityQueue({
26        compare: (a, b) => (a[0] === b[0] ? a[1] - b[1] : a[0] - b[0]),
27    });
28  
29    // Start with source node
30    priorityQueue.enqueue([0, 0]);
31  
32    // Dijkstra's algorithm with disappear time constraint
33    while (priorityQueue.size() > 0) {
34        const [currentDistance, currentNode] = priorityQueue.dequeue()!;
35      
36        // Skip if we've already found a better path to this node
37        if (currentDistance > distances[currentNode]) {
38            continue;
39        }
40      
41        // Explore all neighbors of current node
42        for (const [neighbor, edgeWeight] of adjacencyList[currentNode]) {
43            const newDistance = distances[currentNode] + edgeWeight;
44          
45            // Update distance if we found a shorter path that arrives before node disappears
46            if (distances[neighbor] > newDistance && newDistance < disappear[neighbor]) {
47                distances[neighbor] = newDistance;
48                priorityQueue.enqueue([newDistance, neighbor]);
49            }
50        }
51    }
52  
53    // Convert distances to final result
54    // Return -1 for nodes that cannot be reached before they disappear
55    return distances.map((distance, index) => 
56        distance < disappear[index] ? distance : -1
57    );
58}
59

Time and Space Complexity

Time Complexity: O(m × log m) where m is the number of edges.

The algorithm uses Dijkstra's shortest path algorithm with a min-heap. Breaking down the operations:

  • Building the adjacency list from edges takes O(m) time
  • Each edge can be processed at most twice (once from each endpoint), resulting in at most 2m heap operations
  • Each heap operation (push/pop) takes O(log k) where k is the heap size, which can be at most O(m) in the worst case
  • Therefore, the total time complexity is O(m × log m)

Space Complexity: O(m) where m is the number of edges.

The space usage consists of:

  • The adjacency list g stores all edges twice (once for each direction), using O(m) space
  • The distance array dist uses O(n) space
  • The priority queue pq can contain at most O(m) entries in the worst case
  • Since in a connected graph n ≤ m + 1, and we need at least n - 1 edges to connect n nodes, the dominant term is O(m)
  • Therefore, the total space complexity is O(m)

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

Common Pitfalls

1. Not Handling Multiple Edges Between Same Nodes

The problem explicitly states that the graph might contain multiple edges between the same pair of nodes. A common mistake is assuming there's only one edge between any two nodes and overwriting edges instead of keeping all of them.

Incorrect approach:

# Wrong: This overwrites previous edges
graph = {}
for u, v, weight in edges:
    graph[u] = (v, weight)  # Overwrites!
    graph[v] = (u, weight)  # Overwrites!

Correct approach:

# Correct: Keep all edges, even duplicates
graph = defaultdict(list)
for u, v, weight in edges:
    graph[u].append((v, weight))  # Appends all edges
    graph[v].append((u, weight))  # Appends all edges

2. Incorrect Disappearance Time Check

A critical pitfall is misunderstanding when a node becomes unreachable. The problem states that if you reach a node at exactly the time it disappears or later, you cannot visit it.

Incorrect approach:

# Wrong: Allows visiting at disappearance time
if new_distance <= disappear[neighbor]:  # Should be strict <
    distances[neighbor] = new_distance

Correct approach:

# Correct: Must arrive strictly before disappearance
if new_distance < disappear[neighbor]:  # Strict inequality
    distances[neighbor] = new_distance

3. Not Checking Disappearance Time in Final Result

Even if node 0 has the shortest path calculated, we must verify it's reachable before it disappears. A common mistake is forgetting to check the disappearance constraint when building the final answer.

Incorrect approach:

# Wrong: Returns raw distances without checking disappearance
return [d if d != inf else -1 for d in distances]

Correct approach:

# Correct: Checks both infinity AND disappearance time
result = []
for distance, disappear_time in zip(distances, disappear):
    if distance < disappear_time:  # Must check disappearance
        result.append(distance)
    else:
        result.append(-1)

4. Starting Node Disappearance Edge Case

If disappear[0] == 0, the starting node itself disappears immediately, making the entire graph unreachable. The solution should handle this gracefully.

Solution: The current implementation handles this correctly because the condition new_distance < disappear[neighbor] will prevent any exploration if the starting node has already disappeared, resulting in all nodes (including node 0) being marked as -1 in the final result.

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

Is the following code DFS or BFS?

void search(Node root) {
  if (!root) return;
  visit(root);
  root.visited = true;
  for (Node node in root.adjacent) {
    if (!node.visited) {
      search(node);
    }
  }
}

Recommended Readings

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

Load More