Facebook Pixel

3244. Shortest Distance After Road Addition Queries II

Problem Description

You have n cities numbered from 0 to n - 1. Initially, these cities are connected by unidirectional roads where each city i has a road going to city i + 1 (for all 0 <= i < n - 1). This forms a simple path from city 0 to city n - 1 with initial length n - 1.

You are given a list of queries where each query queries[i] = [u_i, v_i] represents adding a new unidirectional road from city u_i to city v_i. These new roads act as shortcuts that can potentially reduce the distance from city 0 to city n - 1.

After processing each query (adding each new road), you need to determine the length of the shortest path from city 0 to city n - 1.

The problem guarantees that there are no two queries with overlapping but non-nested intervals. Specifically, there won't be cases where queries[i][0] < queries[j][0] < queries[i][1] < queries[j][1].

Your task is to return an array answer where answer[i] represents the shortest path length from city 0 to city n - 1 after processing the first i + 1 queries.

For example, if n = 5, you initially have roads: 0β†’1β†’2β†’3β†’4, giving a path length of 4. If you add a road from city 1 to city 3, you create a shortcut 0β†’1β†’3β†’4, reducing the path length to 3.

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

Intuition

The key insight is that when we add a new road from city u to city v, we're essentially creating a shortcut that allows us to skip cities u+1, u+2, ..., v-1. This shortcut reduces the total path length.

Initially, the shortest path from city 0 to city n-1 requires visiting every city in sequence, giving us a path length of n-1. Each time we add a shortcut, we can potentially reduce this length.

Think of it this way: if we maintain a record of "which city comes next" for each city, we can track the actual path. The array nxt[i] tells us the next city we can reach from city i. Initially, nxt[i] = i + 1 because we can only go to the next consecutive city.

When we add a road from u to v, we update nxt[u] = v, meaning from city u, we can now jump directly to city v. But here's the crucial part: the cities that were previously between u and v in our path are now bypassed. We need to mark these bypassed cities somehow.

The clever trick is to set nxt[i] = 0 for all cities that get bypassed. This indicates that these cities are no longer part of any shortest path from city 0 to city n-1.

By counting how many cities remain "active" (not bypassed) in our path, we can maintain the current shortest distance. Each time we bypass a city, we decrease our path length counter by 1.

There's also an optimization: if we already have a shortcut that covers a larger range, adding a smaller shortcut within that range won't help. For instance, if we already have a road from city 1 to city 5, adding a road from city 2 to city 4 won't reduce the path length further. This is why we check if 0 < nxt[u] < v before processing a query.

Learn more about Greedy and Graph patterns.

Solution Approach

Let's walk through the implementation step by step:

1. Initialize the tracking array: We create an array nxt where nxt[i] represents the next city reachable from city i. Initially, nxt = [1, 2, 3, ..., n-1], meaning each city i connects to city i+1.

2. Initialize the path length counter: We maintain cnt = n - 1, which represents the current shortest path length from city 0 to city n-1.

3. Process each query [u, v]:

For each new road from city u to city v, we first check if this shortcut is meaningful:

  • The condition 0 < nxt[u] < v checks whether:
    • nxt[u] > 0: City u is still active (not bypassed)
    • nxt[u] < v: The new road actually creates a shortcut (jumps further than the current next city)

4. Update bypassed cities: If the shortcut is valid, we need to mark all cities that will be bypassed. Starting from i = nxt[u] (the current next city from u), we iterate through all cities until we reach v:

i = nxt[u]
while i < v:
    cnt -= 1          # Decrease path length for each bypassed city
    nxt[i], i = 0, nxt[i]  # Mark city as bypassed (set to 0) and move to next

The clever use of simultaneous assignment nxt[i], i = 0, nxt[i] ensures we:

  • Save the next city to visit before overwriting it
  • Mark the current city as bypassed by setting nxt[i] = 0
  • Move to the next city in the original path

5. Update the shortcut: After marking all bypassed cities, we set nxt[u] = v to establish the new shortcut.

6. Record the result: After processing each query, we append the current cnt to our answer array.

Example walkthrough: If n = 5 and we add a road from city 1 to city 4:

  • Initial: nxt = [1, 2, 3, 4], cnt = 4
  • Cities 2 and 3 will be bypassed
  • We set nxt[2] = 0, nxt[3] = 0, and nxt[1] = 4
  • cnt decreases by 2 (for the two bypassed cities), becoming 2
  • The new shortest path is: 0 β†’ 1 β†’ 4, with length 2

This greedy approach works because once a city is bypassed, it will never be part of any future shortest path, allowing us to maintain the answer efficiently in O(1) amortized time per query.

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 with n = 6 cities (numbered 0 to 5) and queries [[0, 3], [2, 5], [0, 4]].

Initial Setup:

  • Cities: 0 β†’ 1 β†’ 2 β†’ 3 β†’ 4 β†’ 5
  • nxt = [1, 2, 3, 4, 5] (each city points to the next)
  • cnt = 5 (need 5 steps to go from city 0 to city 5)
  • Path length: 5

Query 1: [0, 3] - Add road from city 0 to city 3

  • Check if shortcut is valid: 0 < nxt[0] < 3 β†’ 0 < 1 < 3 βœ“
  • Cities to bypass: 1 and 2
    • Start with i = nxt[0] = 1
    • While 1 < 3: Set nxt[1] = 0, decrease cnt to 4, move to i = 2
    • While 2 < 3: Set nxt[2] = 0, decrease cnt to 3, move to i = 3
    • Stop (3 is not < 3)
  • Set nxt[0] = 3 (create the shortcut)
  • New path: 0 β†’ 3 β†’ 4 β†’ 5
  • nxt = [3, 0, 0, 4, 5], cnt = 3
  • Answer so far: [3]

Query 2: [2, 5] - Add road from city 2 to city 5

  • Check if shortcut is valid: 0 < nxt[2] < 5 β†’ 0 < 0 < 5 βœ—
  • City 2 is already bypassed (nxt[2] = 0), so this shortcut has no effect
  • Path remains: 0 β†’ 3 β†’ 4 β†’ 5
  • nxt = [3, 0, 0, 4, 5], cnt = 3
  • Answer so far: [3, 3]

Query 3: [0, 4] - Add road from city 0 to city 4

  • Check if shortcut is valid: 0 < nxt[0] < 4 β†’ 0 < 3 < 4 βœ“
  • Cities to bypass: 3
    • Start with i = nxt[0] = 3
    • While 3 < 4: Set nxt[3] = 0, decrease cnt to 2, move to i = 4
    • Stop (4 is not < 4)
  • Set nxt[0] = 4 (create the shortcut)
  • New path: 0 β†’ 4 β†’ 5
  • nxt = [4, 0, 0, 0, 5], cnt = 2
  • Answer so far: [3, 3, 2]

Final Result: [3, 3, 2]

The algorithm efficiently tracks which cities are active in the shortest path. Once a city is bypassed (marked with 0), it never becomes part of the path again, allowing us to maintain the shortest distance by simply counting active transitions.

Solution Implementation

1from typing import List
2
3class Solution:
4    def shortestDistanceAfterQueries(
5        self, n: int, queries: List[List[int]]
6    ) -> List[int]:
7        # Initialize next_node array where next_node[i] points to the next reachable node from i
8        # Initially, each node i points to i+1 (forming a path 0->1->2->...->n-1)
9        next_node = list(range(1, n))
10      
11        # Result array to store shortest distance after each query
12        result = []
13      
14        # Current shortest path length (initially n-1 edges from 0 to n-1)
15        current_distance = n - 1
16      
17        # Process each query which adds a shortcut from u to v
18        for start_node, end_node in queries:
19            # Check if adding this edge creates a valid shortcut
20            # The edge is valid if:
21            # 1. next_node[start_node] is not 0 (not already bypassed)
22            # 2. next_node[start_node] < end_node (creates an actual shortcut)
23            if 0 < next_node[start_node] < end_node:
24                # Start from the node that start_node currently points to
25                current = next_node[start_node]
26              
27                # Mark all nodes between start_node and end_node as bypassed
28                # These nodes are no longer part of the shortest path
29                while current < end_node:
30                    # Decrease the distance count for each bypassed edge
31                    current_distance -= 1
32                    # Mark the node as bypassed (0) and move to the next node
33                    temp = next_node[current]
34                    next_node[current] = 0
35                    current = temp
36              
37                # Update start_node to directly point to end_node (add the shortcut)
38                next_node[start_node] = end_node
39          
40            # Append the current shortest distance after processing this query
41            result.append(current_distance)
42      
43        return result
44
1class Solution {
2    public int[] shortestDistanceAfterQueries(int n, int[][] queries) {
3        // Initialize array to track the next node in the shortest path
4        // nextNode[i] represents the next node from node i in the current shortest path
5        // Initially, each node i points to node i+1 (sequential path)
6        int[] nextNode = new int[n - 1];
7        for (int i = 1; i < n; ++i) {
8            nextNode[i - 1] = i;
9        }
10      
11        // Track the current shortest distance from node 0 to node n-1
12        // Initially it's n-1 (sequential path through all nodes)
13        int currentShortestDistance = n - 1;
14      
15        // Prepare result array to store shortest distance after each query
16        int queryCount = queries.length;
17        int[] result = new int[queryCount];
18      
19        // Process each query (shortcut edge)
20        for (int queryIndex = 0; queryIndex < queryCount; ++queryIndex) {
21            int startNode = queries[queryIndex][0];
22            int endNode = queries[queryIndex][1];
23          
24            // Check if adding this edge creates a valid shortcut
25            // A shortcut is valid if:
26            // 1. The current next node from startNode exists (nextNode[startNode] > 0)
27            // 2. The current next node is before the new endNode
28            if (nextNode[startNode] > 0 && nextNode[startNode] < endNode) {
29                // Remove all intermediate nodes that will be bypassed by this shortcut
30                int currentNode = nextNode[startNode];
31                while (currentNode < endNode) {
32                    // Decrease the shortest distance count for each bypassed node
33                    --currentShortestDistance;
34                  
35                    // Store the next node before marking current as bypassed
36                    int tempNextNode = nextNode[currentNode];
37                  
38                    // Mark this node as bypassed (0 indicates no longer in shortest path)
39                    nextNode[currentNode] = 0;
40                  
41                    // Move to the next node
42                    currentNode = tempNextNode;
43                }
44              
45                // Update the shortcut: startNode now directly connects to endNode
46                nextNode[startNode] = endNode;
47            }
48          
49            // Store the current shortest distance after processing this query
50            result[queryIndex] = currentShortestDistance;
51        }
52      
53        return result;
54    }
55}
56
1class Solution {
2public:
3    vector<int> shortestDistanceAfterQueries(int n, vector<vector<int>>& queries) {
4        // Initialize next array where next[i] points to the next node in the path
5        // Initially forms a linear path: 0 -> 1 -> 2 -> ... -> n-1
6        vector<int> next(n - 1);
7        iota(next.begin(), next.end(), 1);
8      
9        // Track the current shortest distance (number of edges in the path)
10        int currentDistance = n - 1;
11      
12        // Store results after each query
13        vector<int> result;
14      
15        // Process each query that adds a new edge
16        for (const auto& query : queries) {
17            int fromNode = query[0];
18            int toNode = query[1];
19          
20            // Only process if this creates a valid shortcut
21            // Check if fromNode is active and the new edge creates a longer jump
22            if (next[fromNode] != 0 && next[fromNode] < toNode) {
23                // Remove intermediate nodes that will be bypassed
24                int currentNode = next[fromNode];
25                while (currentNode < toNode) {
26                    // Decrease distance count for each bypassed edge
27                    --currentDistance;
28                  
29                    // Save the next node before invalidating current
30                    int nextNode = next[currentNode];
31                  
32                    // Mark this node as bypassed (no longer in the shortest path)
33                    next[currentNode] = 0;
34                  
35                    // Move to the next node
36                    currentNode = nextNode;
37                }
38              
39                // Create the new shortcut edge
40                next[fromNode] = toNode;
41            }
42          
43            // Record the shortest distance after this query
44            result.push_back(currentDistance);
45        }
46      
47        return result;
48    }
49};
50
1/**
2 * Finds the shortest distance after applying each query that adds a shortcut edge
3 * @param n - Number of nodes in the graph (0 to n-1)
4 * @param queries - Array of [u, v] pairs representing new edges from u to v
5 * @returns Array of shortest distances after each query
6 */
7function shortestDistanceAfterQueries(n: number, queries: number[][]): number[] {
8    // Initialize next array where next[i] points to the next node in the path
9    // Initially, each node i points to i+1 (forming a simple path 0->1->2->...->n-1)
10    const nextNode: number[] = Array.from({ length: n - 1 }, (_, index) => index + 1);
11  
12    // Result array to store shortest distance after each query
13    const result: number[] = [];
14  
15    // Current count of edges in the shortest path (initially n-1 edges)
16    let edgeCount: number = n - 1;
17  
18    // Process each query
19    for (const [startNode, endNode] of queries) {
20        // Check if adding edge from startNode to endNode creates a valid shortcut
21        // Only process if startNode has a next node and it's less than endNode
22        if (nextNode[startNode] && nextNode[startNode] < endNode) {
23            // Remove intermediate nodes between startNode and endNode
24            let currentNode: number = nextNode[startNode];
25          
26            // Iterate through all nodes that will be bypassed
27            while (currentNode < endNode) {
28                // Decrease edge count as we're removing this edge
29                --edgeCount;
30              
31                // Store the next node before removing the edge
32                const tempNext: number = nextNode[currentNode];
33              
34                // Mark current node as disconnected (0 means no next node)
35                nextNode[currentNode] = 0;
36              
37                // Move to the next node
38                currentNode = tempNext;
39            }
40          
41            // Create the new shortcut edge from startNode directly to endNode
42            nextNode[startNode] = endNode;
43        }
44      
45        // Add current shortest distance (edge count) to result
46        result.push(edgeCount);
47    }
48  
49    return result;
50}
51

Time and Space Complexity

Time Complexity: O(n + q)

The algorithm initializes a nxt array in O(n) time. For each query, it processes nodes between positions u and v. The key insight is that each node can only be "skipped over" (set to 0) at most once throughout all queries. When nxt[i] is set to 0, that node is effectively removed from future traversals. Since there are n-1 nodes that can be skipped (excluding the first node), and each query takes constant time when no nodes are skipped, the total time across all q queries is bounded by O(n + q).

Space Complexity: O(n)

The algorithm uses a nxt array of size n-1 to track the next reachable city from each position, and an ans array to store results (which has size q). The dominant space usage is the nxt array, giving us O(n) space complexity. The temporary variables i, u, v, and cnt use constant space.

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

Common Pitfalls

1. Incorrect Loop Termination When Marking Bypassed Nodes

The Pitfall: A common mistake is incorrectly handling the loop that marks bypassed nodes. Developers might write:

# WRONG - This can cause infinite loop or skip nodes
current = next_node[start_node]
while current < end_node:
    current_distance -= 1
    next_node[current] = 0
    current = next_node[current]  # Bug: next_node[current] was just set to 0!

The problem here is that after setting next_node[current] = 0, we try to use next_node[current] to get the next node, but it's now 0, causing the loop to terminate prematurely or behave unexpectedly.

The Solution: Use simultaneous assignment or save the next node before modifying:

# CORRECT - Option 1: Simultaneous assignment
current = next_node[start_node]
while current < end_node:
    current_distance -= 1
    next_node[current], current = 0, next_node[current]

# CORRECT - Option 2: Save next node first
current = next_node[start_node]
while current < end_node:
    current_distance -= 1
    temp = next_node[current]
    next_node[current] = 0
    current = temp

2. Not Checking if a Shortcut is Actually Valid

The Pitfall: Blindly adding every query as a shortcut without checking if it actually improves the path:

# WRONG - Processes invalid shortcuts
for start_node, end_node in queries:
    current = next_node[start_node]
    while current < end_node:
        current_distance -= 1
        next_node[current], current = 0, next_node[current]
    next_node[start_node] = end_node
    result.append(current_distance)

This fails when:

  • The node is already bypassed (next_node[start_node] == 0)
  • The shortcut doesn't actually skip any nodes (next_node[start_node] >= end_node)

The Solution: Always validate the shortcut before processing:

# CORRECT - Validates shortcut conditions
for start_node, end_node in queries:
    if 0 < next_node[start_node] < end_node:
        # Process valid shortcut
        current = next_node[start_node]
        while current < end_node:
            current_distance -= 1
            next_node[current], current = 0, next_node[current]
        next_node[start_node] = end_node
    result.append(current_distance)

3. Off-by-One Error in Initial Setup

The Pitfall: Incorrectly initializing the next_node array size or values:

# WRONG - Creates array of wrong size
next_node = list(range(n))  # Should be range(1, n)

# WRONG - Includes n as a destination
next_node = list(range(1, n+1))  # Node n-1 shouldn't point to n

The Solution: Remember that:

  • We have n cities numbered 0 to n-1
  • City n-1 is the destination (doesn't need a next pointer)
  • The array should have n-1 elements
# CORRECT
next_node = list(range(1, n))  # [1, 2, 3, ..., n-1]

This creates an array where next_node[i] represents the next city from city i for i in range [0, n-2].

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

In a binary min heap, the minimum element can be found in:


Recommended Readings

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

Load More