Facebook Pixel

882. Reachable Nodes In Subdivided Graph

Problem Description

You have an undirected graph with n nodes labeled from 0 to n - 1. Each edge in this graph can be subdivided into a chain of smaller edges with new intermediate nodes.

The graph is given as a 2D array edges where each element edges[i] = [ui, vi, cnti] represents:

  • An edge between nodes ui and vi in the original graph
  • cnti is the number of new nodes that will be inserted to subdivide this edge

When you subdivide an edge [ui, vi] with cnti new nodes:

  • The original edge is replaced with (cnti + 1) new edges
  • cnti new nodes x1, x2, ..., xcnti are created
  • The new edges form a chain: [ui, x1], [x1, x2], [x2, x3], ..., [xcnti-1, xcnti], [xcnti, vi]

For example, if an edge [0, 1] has cnt = 2, it becomes: [0, x1], [x1, x2], [x2, 1] with two new intermediate nodes.

After creating this new graph with all the subdivided edges, you need to find how many nodes are reachable from node 0 within maxMoves steps. A node is considered reachable if the shortest distance from node 0 to that node is at most maxMoves.

The problem asks you to return the total count of reachable nodes, which includes:

  • Original nodes that can be reached within maxMoves steps
  • New intermediate nodes created during edge subdivision that can be reached within maxMoves steps

Given:

  • edges: The original graph edges with subdivision counts
  • maxMoves: The maximum number of steps allowed
  • n: The number of nodes in the original graph

Return: The total number of nodes reachable from node 0 in the new subdivided graph.

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 handle two types of nodes differently: the original nodes and the newly created intermediate nodes from edge subdivision.

For the original nodes, we can use Dijkstra's algorithm to find the shortest distance from node 0 to every other node. But here's the trick - when we subdivide an edge with cnt intermediate nodes, the edge weight becomes cnt + 1 (since we need cnt + 1 steps to traverse from one end to the other through all intermediate nodes).

Once we have the shortest distances to all original nodes, counting reachable original nodes is straightforward - any node with distance ≤ maxMoves is reachable.

The challenging part is counting the reachable intermediate nodes. Consider an edge [u, v] that was subdivided with cnt intermediate nodes. These intermediate nodes form a line between u and v. We can reach some of these intermediate nodes from two directions:

  • Starting from node 0, reaching u, then moving into the edge towards v
  • Starting from node 0, reaching v, then moving into the edge towards u

From node u, we can reach at most maxMoves - dist[u] steps into the edge (if we have moves left after reaching u). Similarly, from node v, we can reach at most maxMoves - dist[v] steps into the edge.

The total intermediate nodes we can reach on edge [u, v] is the minimum of:

  • The actual number of intermediate nodes (cnt)
  • The sum of nodes reachable from both ends (a + b), where:
    • a = min(cnt, max(0, maxMoves - dist[u]))
    • b = min(cnt, max(0, maxMoves - dist[v]))

We take the minimum because if a + b > cnt, it means we're counting some nodes from both directions, but the actual number of intermediate nodes is only cnt.

This approach elegantly handles all cases - whether we can reach the entire edge, only parts of it from one or both ends, or none at all.

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

Solution Approach

The implementation uses Dijkstra's algorithm with a min-heap to find shortest distances, followed by counting reachable nodes.

Step 1: Build the Graph

g = defaultdict(list)
for u, v, cnt in edges:
    g[u].append((v, cnt + 1))
    g[v].append((u, cnt + 1))

We create an adjacency list where each edge weight is cnt + 1 (the number of steps needed to traverse the subdivided edge). Since the graph is undirected, we add edges in both directions.

Step 2: Run Dijkstra's Algorithm

q = [(0, 0)]  # (distance, node)
dist = [0] + [inf] * n
while q:
    d, u = heappop(q)
    for v, cnt in g[u]:
        if (t := d + cnt) < dist[v]:
            dist[v] = t
            q.append((t, v))
  • Initialize a min-heap with (0, 0) - starting from node 0 with distance 0
  • Initialize distance array: dist[0] = 0 and all others to infinity
  • Process nodes in order of increasing distance
  • For each neighbor v of current node u, if we find a shorter path, update dist[v] and add to heap
  • Note: This implementation uses a simple list as heap without visited tracking, which works but may process nodes multiple times

Step 3: Count Reachable Original Nodes

ans = sum(d <= maxMoves for d in dist)

Count all original nodes whose shortest distance from node 0 is at most maxMoves.

Step 4: Count Reachable Intermediate Nodes

for u, v, cnt in edges:
    a = min(cnt, max(0, maxMoves - dist[u]))
    b = min(cnt, max(0, maxMoves - dist[v]))
    ans += min(cnt, a + b)

For each original edge with cnt intermediate nodes:

  • a: Maximum intermediate nodes reachable from u side = min(cnt, max(0, maxMoves - dist[u]))
    • We have maxMoves - dist[u] moves left after reaching u
    • Can't go negative (hence max(0, ...))
    • Can't exceed the actual number of intermediate nodes (hence min(cnt, ...))
  • b: Similarly for the v side
  • Total reachable on this edge = min(cnt, a + b)
    • If a + b ≤ cnt: We can reach a nodes from u side and b from v side without overlap
    • If a + b > cnt: The entire edge is reachable, so we count all cnt intermediate nodes

The algorithm runs in O(E log V) time for Dijkstra's algorithm plus O(E) for counting intermediate nodes, where E is the number of edges and V is the number of vertices.

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 small example to illustrate the solution approach.

Given:

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

Step 1: Build the Graph

After subdivision, each edge weight becomes cnt + 1:

  • Edge [0,1] with cnt=4 → weight = 5 (needs 5 moves to traverse)
  • Edge [1,2] with cnt=6 → weight = 7
  • Edge [0,2] with cnt=8 → weight = 9
  • Edge [1,3] with cnt=1 → weight = 2

The graph looks like:

    0 ---(5)--- 1 ---(2)--- 3
     \         /
      \       /
       (9)  (7)
         \ /
          2

Step 2: Run Dijkstra's Algorithm

Starting from node 0:

  • Process node 0 (dist=0):
    • To node 1: dist[1] = 0 + 5 = 5
    • To node 2: dist[2] = 0 + 9 = 9
  • Process node 1 (dist=5):
    • To node 2: 5 + 7 = 12 > 9 (no update)
    • To node 3: dist[3] = 5 + 2 = 7
  • Process node 3 (dist=7): no improvements
  • Process node 2 (dist=9): no improvements

Final distances: dist = [0, 5, 9, 7]

Step 3: Count Reachable Original Nodes

Nodes with distance ≤ 10:

  • Node 0: dist=0 ✓
  • Node 1: dist=5 ✓
  • Node 2: dist=9 ✓
  • Node 3: dist=7 ✓

All 4 original nodes are reachable. Count = 4.

Step 4: Count Reachable Intermediate Nodes

For edge [0,1] with 4 intermediate nodes:

  • From node 0: can go min(4, max(0, 10-0)) = min(4, 10) = 4 steps
  • From node 1: can go min(4, max(0, 10-5)) = min(4, 5) = 4 steps
  • Total: min(4, 4+4) = min(4, 8) = 4 intermediate nodes

For edge [1,2] with 6 intermediate nodes:

  • From node 1: can go min(6, max(0, 10-5)) = min(6, 5) = 5 steps
  • From node 2: can go min(6, max(0, 10-9)) = min(6, 1) = 1 step
  • Total: min(6, 5+1) = min(6, 6) = 6 intermediate nodes

For edge [0,2] with 8 intermediate nodes:

  • From node 0: can go min(8, max(0, 10-0)) = min(8, 10) = 8 steps
  • From node 2: can go min(8, max(0, 10-9)) = min(8, 1) = 1 step
  • Total: min(8, 8+1) = min(8, 9) = 8 intermediate nodes

For edge [1,3] with 1 intermediate node:

  • From node 1: can go min(1, max(0, 10-5)) = min(1, 5) = 1 step
  • From node 3: can go min(1, max(0, 10-7)) = min(1, 3) = 1 step
  • Total: min(1, 1+1) = min(1, 2) = 1 intermediate node

Final Answer:

  • Original nodes: 4
  • Intermediate nodes: 4 + 6 + 8 + 1 = 19
  • Total reachable nodes: 4 + 19 = 23

Solution Implementation

1class Solution:
2    def reachableNodes(self, edges: List[List[int]], maxMoves: int, n: int) -> int:
3        # Build adjacency list representation of the graph
4        # Each edge has 'cnt' intermediate nodes, so total distance is cnt + 1
5        graph = defaultdict(list)
6        for start_node, end_node, intermediate_count in edges:
7            graph[start_node].append((end_node, intermediate_count + 1))
8            graph[end_node].append((start_node, intermediate_count + 1))
9      
10        # Initialize priority queue for Dijkstra's algorithm
11        # Format: (distance, node)
12        priority_queue = [(0, 0)]
13      
14        # Initialize distances array (distance from node 0 to all other nodes)
15        # dist[0] = 0 (starting node), all others initialized to infinity
16        distances = [0] + [float('inf')] * n
17      
18        # Run Dijkstra's algorithm to find shortest distances from node 0
19        while priority_queue:
20            current_distance, current_node = heappop(priority_queue)
21          
22            # Explore neighbors of current node
23            for neighbor, edge_weight in graph[current_node]:
24                new_distance = current_distance + edge_weight
25              
26                # If we found a shorter path to the neighbor, update it
27                if new_distance < distances[neighbor]:
28                    distances[neighbor] = new_distance
29                    heappush(priority_queue, (new_distance, neighbor))
30      
31        # Count main nodes that are reachable within maxMoves
32        reachable_count = sum(distance <= maxMoves for distance in distances)
33      
34        # Count intermediate nodes on edges that can be reached
35        for start_node, end_node, intermediate_count in edges:
36            # Calculate how many intermediate nodes can be reached from start_node
37            reachable_from_start = min(intermediate_count, max(0, maxMoves - distances[start_node]))
38          
39            # Calculate how many intermediate nodes can be reached from end_node
40            reachable_from_end = min(intermediate_count, max(0, maxMoves - distances[end_node]))
41          
42            # Total intermediate nodes reached on this edge (cannot exceed the actual count)
43            reachable_count += min(intermediate_count, reachable_from_start + reachable_from_end)
44      
45        return reachable_count
46
1class Solution {
2    public int reachableNodes(int[][] edges, int maxMoves, int n) {
3        // Build adjacency list representation of the graph
4        // Each node stores list of [neighbor, distance to neighbor]
5        List<int[]>[] graph = new List[n];
6        Arrays.setAll(graph, index -> new ArrayList<>());
7      
8        // Convert edges to adjacency list
9        // Note: distance = subdivided nodes + 1 (to account for moving through subdivisions)
10        for (int[] edge : edges) {
11            int nodeU = edge[0];
12            int nodeV = edge[1];
13            int distance = edge[2] + 1;  // Number of subdivided nodes + 1
14          
15            graph[nodeU].add(new int[] {nodeV, distance});
16            graph[nodeV].add(new int[] {nodeU, distance});
17        }
18      
19        // Initialize distances array for Dijkstra's algorithm
20        int[] distances = new int[n];
21        Arrays.fill(distances, 1 << 30);  // Initialize with large value (infinity)
22        distances[0] = 0;  // Starting node has distance 0
23      
24        // Priority queue for Dijkstra's algorithm: [distance, node]
25        PriorityQueue<int[]> priorityQueue = new PriorityQueue<>((a, b) -> a[0] - b[0]);
26        priorityQueue.offer(new int[] {0, 0});  // Start from node 0 with distance 0
27      
28        // Run Dijkstra's algorithm to find shortest distances
29        while (!priorityQueue.isEmpty()) {
30            int[] current = priorityQueue.poll();
31            int currentDistance = current[0];
32            int currentNode = current[1];
33          
34            // Check all neighbors of current node
35            for (int[] neighbor : graph[currentNode]) {
36                int neighborNode = neighbor[0];
37                int edgeDistance = neighbor[1];
38              
39                // If we found a shorter path to the neighbor, update it
40                if (currentDistance + edgeDistance < distances[neighborNode]) {
41                    distances[neighborNode] = currentDistance + edgeDistance;
42                    priorityQueue.offer(new int[] {distances[neighborNode], neighborNode});
43                }
44            }
45        }
46      
47        // Count reachable original nodes
48        int totalReachableNodes = 0;
49        for (int distance : distances) {
50            if (distance <= maxMoves) {
51                totalReachableNodes++;
52            }
53        }
54      
55        // Count reachable subdivided nodes on each edge
56        for (int[] edge : edges) {
57            int nodeU = edge[0];
58            int nodeV = edge[1];
59            int subdividedNodes = edge[2];  // Number of subdivided nodes on this edge
60          
61            // Calculate how many subdivided nodes can be reached from nodeU
62            int reachableFromU = Math.min(subdividedNodes, Math.max(0, maxMoves - distances[nodeU]));
63          
64            // Calculate how many subdivided nodes can be reached from nodeV
65            int reachableFromV = Math.min(subdividedNodes, Math.max(0, maxMoves - distances[nodeV]));
66          
67            // Total reachable subdivided nodes on this edge (avoiding double counting)
68            totalReachableNodes += Math.min(subdividedNodes, reachableFromU + reachableFromV);
69        }
70      
71        return totalReachableNodes;
72    }
73}
74
1class Solution {
2public:
3    int reachableNodes(vector<vector<int>>& edges, int maxMoves, int n) {
4        // Build adjacency list representation of the graph
5        // Each edge has small nodes between two big nodes
6        using PairIntInt = pair<int, int>;
7        vector<vector<PairIntInt>> adjacencyList(n);
8      
9        for (const auto& edge : edges) {
10            int nodeU = edge[0];
11            int nodeV = edge[1];
12            int smallNodeCount = edge[2];
13            // Add 1 to represent the cost to traverse from one big node to another
14            int traversalCost = smallNodeCount + 1;
15          
16            adjacencyList[nodeU].emplace_back(nodeV, traversalCost);
17            adjacencyList[nodeV].emplace_back(nodeU, traversalCost);
18        }
19      
20        // Dijkstra's algorithm to find shortest distances from node 0
21        priority_queue<PairIntInt, vector<PairIntInt>, greater<PairIntInt>> minHeap;
22        minHeap.emplace(0, 0); // (distance, node)
23      
24        // Initialize distances with a large value
25        vector<int> shortestDistance(n, INT_MAX);
26        shortestDistance[0] = 0;
27      
28        // Standard Dijkstra's algorithm
29        while (!minHeap.empty()) {
30            auto [currentDistance, currentNode] = minHeap.top();
31            minHeap.pop();
32          
33            // Skip if we've already found a better path to this node
34            if (currentDistance > shortestDistance[currentNode]) {
35                continue;
36            }
37          
38            // Explore neighbors
39            for (const auto& [neighbor, edgeCost] : adjacencyList[currentNode]) {
40                int newDistance = currentDistance + edgeCost;
41              
42                if (newDistance < shortestDistance[neighbor]) {
43                    shortestDistance[neighbor] = newDistance;
44                    minHeap.emplace(newDistance, neighbor);
45                }
46            }
47        }
48      
49        // Count reachable big nodes
50        int reachableNodeCount = 0;
51        for (int distance : shortestDistance) {
52            if (distance <= maxMoves) {
53                reachableNodeCount++;
54            }
55        }
56      
57        // Count reachable small nodes on each edge
58        for (const auto& edge : edges) {
59            int nodeU = edge[0];
60            int nodeV = edge[1];
61            int smallNodesOnEdge = edge[2];
62          
63            // Calculate how many small nodes can be reached from nodeU
64            int reachableFromU = min(smallNodesOnEdge, 
65                                    max(0, maxMoves - shortestDistance[nodeU]));
66          
67            // Calculate how many small nodes can be reached from nodeV
68            int reachableFromV = min(smallNodesOnEdge, 
69                                    max(0, maxMoves - shortestDistance[nodeV]));
70          
71            // Total reachable small nodes on this edge (avoiding double counting)
72            reachableNodeCount += min(smallNodesOnEdge, reachableFromU + reachableFromV);
73        }
74      
75        return reachableNodeCount;
76    }
77};
78
1function reachableNodes(edges: number[][], maxMoves: number, n: number): number {
2    // Build adjacency list representation of the graph
3    // Each edge has small nodes between two big nodes
4    const adjacencyList: Array<Array<[number, number]>> = Array(n).fill(null).map(() => []);
5  
6    for (const edge of edges) {
7        const nodeU = edge[0];
8        const nodeV = edge[1];
9        const smallNodeCount = edge[2];
10        // Add 1 to represent the cost to traverse from one big node to another
11        const traversalCost = smallNodeCount + 1;
12      
13        adjacencyList[nodeU].push([nodeV, traversalCost]);
14        adjacencyList[nodeV].push([nodeU, traversalCost]);
15    }
16  
17    // Dijkstra's algorithm to find shortest distances from node 0
18    // Using a min heap implemented with array and sort
19    const minHeap: Array<[number, number]> = [[0, 0]]; // [distance, node]
20  
21    // Initialize distances with a large value
22    const shortestDistance: number[] = Array(n).fill(Number.MAX_SAFE_INTEGER);
23    shortestDistance[0] = 0;
24  
25    // Standard Dijkstra's algorithm
26    while (minHeap.length > 0) {
27        // Sort to maintain min heap property
28        minHeap.sort((a, b) => a[0] - b[0]);
29        const [currentDistance, currentNode] = minHeap.shift()!;
30      
31        // Skip if we've already found a better path to this node
32        if (currentDistance > shortestDistance[currentNode]) {
33            continue;
34        }
35      
36        // Explore neighbors
37        for (const [neighbor, edgeCost] of adjacencyList[currentNode]) {
38            const newDistance = currentDistance + edgeCost;
39          
40            if (newDistance < shortestDistance[neighbor]) {
41                shortestDistance[neighbor] = newDistance;
42                minHeap.push([newDistance, neighbor]);
43            }
44        }
45    }
46  
47    // Count reachable big nodes
48    let reachableNodeCount = 0;
49    for (const distance of shortestDistance) {
50        if (distance <= maxMoves) {
51            reachableNodeCount++;
52        }
53    }
54  
55    // Count reachable small nodes on each edge
56    for (const edge of edges) {
57        const nodeU = edge[0];
58        const nodeV = edge[1];
59        const smallNodesOnEdge = edge[2];
60      
61        // Calculate how many small nodes can be reached from nodeU
62        const reachableFromU = Math.min(
63            smallNodesOnEdge,
64            Math.max(0, maxMoves - shortestDistance[nodeU])
65        );
66      
67        // Calculate how many small nodes can be reached from nodeV
68        const reachableFromV = Math.min(
69            smallNodesOnEdge,
70            Math.max(0, maxMoves - shortestDistance[nodeV])
71        );
72      
73        // Total reachable small nodes on this edge (avoiding double counting)
74        reachableNodeCount += Math.min(smallNodesOnEdge, reachableFromU + reachableFromV);
75    }
76  
77    return reachableNodeCount;
78}
79

Time and Space Complexity

Time Complexity: O((E + V) * log V) where E is the number of edges and V is the number of vertices (n).

  • Building the adjacency list takes O(E) time as we iterate through all edges once.
  • The Dijkstra's algorithm implementation uses a min-heap. In the worst case, each edge can be relaxed once, leading to O(E) heap operations.
  • Each heap operation (push/pop) takes O(log V) time since the heap can contain at most V elements at any time.
  • The final loop to count reachable nodes within edges takes O(E) time.
  • Therefore, the dominant operation is the Dijkstra's algorithm portion: O(E * log V).
  • Since we also iterate through vertices for initialization and counting, the total complexity is O((E + V) * log V).

Space Complexity: O(E + V)

  • The adjacency list g stores all edges twice (bidirectional), requiring O(E) space.
  • The distance array dist requires O(V) space.
  • The priority queue q can contain at most O(V) elements in the worst case.
  • Overall space complexity is O(E + V).

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

Common Pitfalls

1. Incorrect Dijkstra's Implementation - Missing Visited Check

The provided code has a subtle but critical bug in its Dijkstra's implementation. It uses heappush instead of checking if a node has already been processed with a better distance.

The Problem:

# Current problematic code
while priority_queue:
    current_distance, current_node = heappop(priority_queue)
    for neighbor, edge_weight in graph[current_node]:
        new_distance = current_distance + edge_weight
        if new_distance < distances[neighbor]:
            distances[neighbor] = new_distance
            heappush(priority_queue, (new_distance, neighbor))  # Bug here!

This implementation can process the same node multiple times with outdated distances. When we pop a node from the heap, it might have an outdated (larger) distance if we've already found a better path to it. This leads to:

  • Incorrect distance calculations
  • Processing nodes with stale distance values
  • Potential TLE in worst cases

The Solution:

while priority_queue:
    current_distance, current_node = heappop(priority_queue)
  
    # Skip if we've already found a better path to this node
    if current_distance > distances[current_node]:
        continue
  
    for neighbor, edge_weight in graph[current_node]:
        new_distance = current_distance + edge_weight
        if new_distance < distances[neighbor]:
            distances[neighbor] = new_distance
            heappush(priority_queue, (new_distance, neighbor))

2. Off-by-One Error in Distance Initialization

Another issue in the original code:

# Problematic initialization
distances = [0] + [float('inf')] * n

This creates a list of size n + 1 when it should be size n. The nodes are labeled from 0 to n-1, so we need exactly n positions.

The Solution:

distances = [float('inf')] * n
distances[0] = 0

3. Edge Weight Confusion

A common conceptual mistake is confusing the number of intermediate nodes with the edge weight:

  • If an edge has cnt intermediate nodes, the total distance to traverse it is cnt + 1
  • Many incorrectly use cnt as the edge weight directly

Correct approach:

# Each edge with 'cnt' intermediate nodes has weight cnt + 1
graph[start_node].append((end_node, intermediate_count + 1))

Complete Corrected Solution:

class Solution:
    def reachableNodes(self, edges: List[List[int]], maxMoves: int, n: int) -> int:
        from collections import defaultdict
        from heapq import heappush, heappop
      
        # Build adjacency list
        graph = defaultdict(list)
        for u, v, cnt in edges:
            graph[u].append((v, cnt + 1))
            graph[v].append((u, cnt + 1))
      
        # Dijkstra's algorithm
        pq = [(0, 0)]  # (distance, node)
        dist = [float('inf')] * n
        dist[0] = 0
      
        while pq:
            d, u = heappop(pq)
          
            # Skip if we've already processed this node with a better distance
            if d > dist[u]:
                continue
          
            for v, weight in graph[u]:
                new_dist = d + weight
                if new_dist < dist[v]:
                    dist[v] = new_dist
                    heappush(pq, (new_dist, v))
      
        # Count reachable original nodes
        result = sum(d <= maxMoves for d in dist)
      
        # Count reachable intermediate nodes
        for u, v, cnt in edges:
            # Nodes reachable from u side
            from_u = min(cnt, max(0, maxMoves - dist[u]))
            # Nodes reachable from v side  
            from_v = min(cnt, max(0, maxMoves - dist[v]))
            # Total intermediate nodes reachable on this edge
            result += min(cnt, from_u + from_v)
      
        return result
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following shows the order of node visit in a Breadth-first Search?


Recommended Readings

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

Load More