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 nodei
from node 0answer[i]
is-1
if nodei
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.
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:
- The new distance
dist[u] + w
is shorter than the current known distancedist[v]
- 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)
tog[u]
- Add
(u, w)
tog[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:
-
Extract minimum: Pop the node
u
with the smallest distancedu
from the heap usingheappop(pq)
. -
Skip outdated entries: If
du > dist[u]
, this entry is outdated (we've already found a shorter path tou
), so skip it and continue. -
Relax edges: For each neighbor
v
of nodeu
with edge weightw
:- 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]
- Is this path shorter?
- If both conditions are satisfied:
- Update
dist[v] = new_dist
- Push
(dist[v], v)
to the priority queue
- Update
- Calculate the new potential distance:
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, soanswer[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 EvaluatorExample 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
- Node 1: new_dist = 0 + 2 = 2
- 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)
wherek
is the heap size, which can be at mostO(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), usingO(m)
space - The distance array
dist
usesO(n)
space - The priority queue
pq
can contain at mostO(m)
entries in the worst case - Since in a connected graph
n ≤ m + 1
, and we need at leastn - 1
edges to connectn
nodes, the dominant term isO(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.
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
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Shortest Path Between A and B Prereq BFS on Graph problems graph_bfs Given an unweighted connected graph return the length of the shortest path between two nodes A and B in terms of the number of edges Assume there always exists a path between nodes A and B Input graph
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Want a Structured Path to Master System Design Too? Don’t Miss This!