Facebook Pixel

3243. Shortest Distance After Road Addition Queries I

Problem Description

You have n cities numbered from 0 to n - 1. Initially, these cities are connected by one-way 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.

You're given a list of queries where each query queries[i] = [u_i, v_i] represents adding a new one-way road from city u_i to city v_i. These new roads act as shortcuts that can potentially reduce the travel distance between cities.

Your task is to process these queries one by one. After adding each new road, you need to find the shortest path from city 0 to city n - 1. The result should be an array where answer[i] represents the length of the shortest path from city 0 to city n - 1 after processing the first i + 1 queries.

For example, if n = 5, initially you have roads: 0β†’1β†’2β†’3β†’4, giving a shortest path length of 4. If the first query adds a road from city 0 to city 3, then the new shortest path would be 0β†’3β†’4 with length 2.

The key points to understand:

  • Roads are unidirectional (one-way only)
  • Initially, there's a sequential path from city 0 to city n - 1
  • Each query adds a new road that might create a shortcut
  • You need to calculate the shortest path after each query is processed
  • The path length is the number of roads traveled, not the number of cities visited

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 involves cities (nodes) connected by roads (edges). We have a directed graph where initially each city i connects to city i+1, and we're adding new edges with each query.

Is it a tree?

  • No: The graph is not a tree because after adding new roads from queries, we can have multiple paths between cities and potentially create cycles.

Is the problem related to directed acyclic graphs?

  • No: While the graph is directed, it's not specifically about DAG properties like topological ordering.

Is the problem related to shortest paths?

  • Yes: We need to find the shortest path from city 0 to city n-1 after each query.

Is the graph weighted?

  • No: All roads have the same weight (cost of 1). We're counting the number of roads traveled, not summing weights.

BFS

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

Conclusion: The flowchart correctly leads us to use BFS (Breadth-First Search) for finding the shortest path in an unweighted directed graph. BFS guarantees finding the shortest path in terms of the number of edges when all edges have equal weight.

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

Intuition

When we think about this problem, we start with a simple chain of cities connected sequentially: 0β†’1β†’2β†’...β†’(n-1). Each query adds a new "shortcut" road that might help us reach the destination faster.

The key insight is that after adding each new road, we need to recalculate the shortest path because this new road might create a better route. Since all roads have the same "cost" (each road counts as 1 in the path length), this is an unweighted graph problem.

Why BFS? In an unweighted graph, BFS naturally finds the shortest path by exploring nodes level by level. When BFS first reaches a node, it's guaranteed to have found the shortest path to that node. This property makes BFS perfect for our problem.

Here's the thought process:

  1. We maintain an adjacency list g where g[i] stores all cities directly reachable from city i
  2. Initially, g[i] = [i+1] for all cities except the last one
  3. For each query [u, v], we add v to g[u], creating a new road
  4. After adding each road, we run BFS from city 0 to find the shortest distance to city n-1

The BFS works by:

  • Starting from city 0 with distance 0
  • Exploring all cities at distance 1, then distance 2, and so on
  • When we first reach city n-1, we've found the shortest path

This approach is straightforward because we don't need to optimize for weighted edges or worry about negative cycles - we simply need to count the minimum number of "hops" from start to end after each road addition.

Learn more about Breadth-First Search and Graph patterns.

Solution Approach

The implementation follows a straightforward BFS approach with dynamic graph updates:

Graph Representation: We use an adjacency list g where g[i] is a list containing all cities directly reachable from city i. Initially, we build this graph with:

g = [[i + 1] for i in range(n - 1)]

This creates the initial roads where city i connects to city i + 1.

Processing Queries: For each query [u, v], we:

  1. Add the new road by appending v to g[u]
  2. Run BFS to find the new shortest distance from city 0 to city n-1
  3. Store this distance in our answer array

BFS Implementation: The BFS function works as follows:

  1. Initialization: Start with city 0 in the queue, mark it as visited, and set distance d = 0

  2. Level-by-level exploration: Process all nodes at the current distance level before moving to the next level

    for _ in range(len(q)):
        u = q.popleft()
  3. Target check: When we dequeue city n-1, we immediately return the current distance d as it's guaranteed to be the shortest

  4. Neighbor exploration: For each unvisited neighbor v of the current city u:

    • Mark v as visited
    • Add v to the queue for the next level
  5. Distance increment: After processing all nodes at the current level, increment d by 1

The use of a vis array prevents revisiting cities and ensures we find the shortest path. Since BFS explores nodes level by level, the first time we reach the destination is guaranteed to be via the shortest path.

Time Complexity: For each query, BFS takes O(n + edges) time. With q queries, the total time is O(q Γ— (n + edges)).

Space Complexity: O(n) for the graph representation and BFS data structures.

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

Initial Setup:

  • Cities: 0, 1, 2, 3, 4
  • Initial roads: 0β†’1, 1β†’2, 2β†’3, 3β†’4
  • Graph representation: g = [[1], [2], [3], [4], []]
  • Initial shortest path: 0β†’1β†’2β†’3β†’4 (length = 4)

Processing Query 1: [0, 3]

  1. Add road from city 0 to city 3: g[0].append(3), so g = [[1, 3], [2], [3], [4], []]
  2. Run BFS from city 0:
    • Start: queue = [0], visited = {0}, distance = 0
    • Level 0: Process city 0
      • Neighbors: 1 and 3 (both unvisited)
      • Add to queue: [1, 3], visited = {0, 1, 3}
    • Level 1: Process cities 1 and 3
      • From city 1: neighbor 2 (unvisited) β†’ queue = [3, 2]
      • From city 3: neighbor 4 (unvisited) β†’ queue = [2, 4]
      • Combined queue after level: [2, 4], visited = {0, 1, 2, 3, 4}
    • Level 2: Process cities 2 and 4
      • City 4 is our target (n-1)! Return distance = 2
  3. New shortest path: 0β†’3β†’4 (length = 2)
  4. Answer so far: [2]

Processing Query 2: [2, 4]

  1. Add road from city 2 to city 4: g[2].append(4), so g = [[1, 3], [2], [3, 4], [4], []]
  2. Run BFS from city 0:
    • Start: queue = [0], visited = {0}, distance = 0
    • Level 0: Process city 0
      • Neighbors: 1 and 3 β†’ queue = [1, 3], visited = {0, 1, 3}
    • Level 1: Process cities 1 and 3
      • From city 1: neighbor 2 β†’ queue = [3, 2]
      • From city 3: neighbor 4 β†’ queue = [2, 4]
      • Combined queue after level: [2, 4], visited = {0, 1, 2, 3, 4}
    • Level 2: Process cities 2 and 4
      • City 4 is our target! Return distance = 2
  3. Shortest path remains: 0β†’3β†’4 (length = 2)
    • Note: The new road 2β†’4 doesn't improve our shortest path
  4. Final answer: [2, 2]

Key Observations:

  • The first query created a significant shortcut, reducing the path from 4 to 2
  • The second query didn't improve the shortest path since we already had a 2-hop route
  • BFS efficiently finds the shortest path by exploring all cities at distance d before moving to distance d+1
  • When BFS first encounters city n-1, we're guaranteed to have found the shortest path

Solution Implementation

1from collections import deque
2from typing import List
3
4class Solution:
5    def shortestDistanceAfterQueries(
6        self, n: int, queries: List[List[int]]
7    ) -> List[int]:
8        """
9        Find shortest distances from node 0 to node n-1 after each query.
10        Each query adds a new directed edge to the graph.
11      
12        Args:
13            n: Number of nodes in the graph (0 to n-1)
14            queries: List of [u, v] pairs representing edges to add
15      
16        Returns:
17            List of shortest distances after each query
18        """
19      
20        def bfs(start: int) -> int:
21            """
22            Perform BFS to find shortest distance from start node to node n-1.
23          
24            Args:
25                start: Starting node for BFS
26          
27            Returns:
28                Shortest distance from start to node n-1
29            """
30            # Initialize queue with starting node
31            queue = deque([start])
32          
33            # Track visited nodes to avoid cycles
34            visited = [False] * n
35            visited[start] = True
36          
37            # Distance from start node
38            distance = 0
39          
40            # BFS traversal
41            while True:
42                # Process all nodes at current distance level
43                for _ in range(len(queue)):
44                    current_node = queue.popleft()
45                  
46                    # Check if we reached the target node (n-1)
47                    if current_node == n - 1:
48                        return distance
49                  
50                    # Add unvisited neighbors to queue
51                    for neighbor in adjacency_list[current_node]:
52                        if not visited[neighbor]:
53                            visited[neighbor] = True
54                            queue.append(neighbor)
55              
56                # Increment distance for next level
57                distance += 1
58      
59        # Initialize adjacency list with default edges (i -> i+1)
60        # Each node initially connects to the next node in sequence
61        adjacency_list = [[i + 1] for i in range(n - 1)]
62      
63        # Store results after each query
64        result = []
65      
66        # Process each query
67        for source, destination in queries:
68            # Add new edge to the graph
69            adjacency_list[source].append(destination)
70          
71            # Calculate shortest distance after adding this edge
72            shortest_distance = bfs(0)
73            result.append(shortest_distance)
74      
75        return result
76
1class Solution {
2    // Adjacency list representation of the graph
3    private List<Integer>[] adjacencyList;
4    // Number of nodes in the graph
5    private int nodeCount;
6
7    /**
8     * Finds shortest distances from node 0 to node n-1 after each query.
9     * Initially, the graph has edges from i to i+1 for all 0 <= i < n-1.
10     * Each query adds a new edge from queries[i][0] to queries[i][1].
11     * 
12     * @param n The number of nodes in the graph (0 to n-1)
13     * @param queries Array of edges to be added, where queries[i] = [u, v] adds edge u -> v
14     * @return Array of shortest distances from node 0 to node n-1 after each query
15     */
16    public int[] shortestDistanceAfterQueries(int n, int[][] queries) {
17        this.nodeCount = n;
18      
19        // Initialize adjacency list for each node
20        adjacencyList = new List[n];
21        Arrays.setAll(adjacencyList, index -> new ArrayList<>());
22      
23        // Create initial edges: 0->1, 1->2, ..., (n-2)->(n-1)
24        for (int i = 0; i < n - 1; i++) {
25            adjacencyList[i].add(i + 1);
26        }
27      
28        int queryCount = queries.length;
29        int[] result = new int[queryCount];
30      
31        // Process each query by adding the new edge and finding shortest path
32        for (int i = 0; i < queryCount; i++) {
33            int sourceNode = queries[i][0];
34            int destinationNode = queries[i][1];
35          
36            // Add the new edge to the graph
37            adjacencyList[sourceNode].add(destinationNode);
38          
39            // Find shortest path from node 0 to node n-1 using BFS
40            result[i] = findShortestPath(0);
41        }
42      
43        return result;
44    }
45
46    /**
47     * Performs BFS to find the shortest path from start node to the last node (n-1).
48     * 
49     * @param startNode The starting node for BFS (typically 0)
50     * @return The shortest distance from startNode to node n-1
51     */
52    private int findShortestPath(int startNode) {
53        // Queue for BFS traversal
54        Deque<Integer> queue = new ArrayDeque<>();
55        queue.offer(startNode);
56      
57        // Track visited nodes to avoid cycles
58        boolean[] visited = new boolean[nodeCount];
59        visited[startNode] = true;
60      
61        // BFS level by level to find shortest path
62        for (int distance = 0; ; distance++) {
63            // Process all nodes at current distance level
64            int nodesAtCurrentLevel = queue.size();
65          
66            for (int i = 0; i < nodesAtCurrentLevel; i++) {
67                int currentNode = queue.poll();
68              
69                // Check if we've reached the destination
70                if (currentNode == nodeCount - 1) {
71                    return distance;
72                }
73              
74                // Explore all neighbors of current node
75                for (int neighbor : adjacencyList[currentNode]) {
76                    if (!visited[neighbor]) {
77                        visited[neighbor] = true;
78                        queue.offer(neighbor);
79                    }
80                }
81            }
82        }
83    }
84}
85
1class Solution {
2public:
3    vector<int> shortestDistanceAfterQueries(int n, vector<vector<int>>& queries) {
4        // Initialize adjacency list for the graph
5        // Initially, each city i has a road to city i+1
6        vector<vector<int>> graph(n);
7        for (int i = 0; i < n - 1; ++i) {
8            graph[i].push_back(i + 1);
9        }
10      
11        // BFS function to find shortest distance from start to city n-1
12        auto bfs = [&](int start) -> int {
13            queue<int> bfsQueue;
14            bfsQueue.push(start);
15          
16            // Track visited nodes to avoid cycles
17            vector<bool> visited(n, false);
18            visited[start] = true;
19          
20            // Level-order BFS to track distance
21            int distance = 0;
22            while (true) {
23                // Process all nodes at current distance level
24                int levelSize = bfsQueue.size();
25                for (int i = 0; i < levelSize; ++i) {
26                    int currentNode = bfsQueue.front();
27                    bfsQueue.pop();
28                  
29                    // Check if we reached the destination
30                    if (currentNode == n - 1) {
31                        return distance;
32                    }
33                  
34                    // Explore all neighbors
35                    for (int neighbor : graph[currentNode]) {
36                        if (!visited[neighbor]) {
37                            visited[neighbor] = true;
38                            bfsQueue.push(neighbor);
39                        }
40                    }
41                }
42                // Move to next distance level
43                distance++;
44            }
45        };
46      
47        // Process each query and compute shortest distance after adding new road
48        vector<int> result;
49        for (const auto& query : queries) {
50            // Add new road from query[0] to query[1]
51            int from = query[0];
52            int to = query[1];
53            graph[from].push_back(to);
54          
55            // Calculate shortest distance from city 0 to city n-1
56            result.push_back(bfs(0));
57        }
58      
59        return result;
60    }
61};
62
1/**
2 * Finds the shortest distance from node 0 to node n-1 after each query.
3 * Each query adds a new edge to the graph.
4 * 
5 * @param n - The number of nodes in the graph (0 to n-1)
6 * @param queries - Array of queries, each query [u, v] adds an edge from u to v
7 * @returns Array of shortest distances after each query
8 */
9function shortestDistanceAfterQueries(n: number, queries: number[][]): number[] {
10    // Initialize adjacency list for the graph
11    // Initially, each node i has an edge to node i+1 (except the last node)
12    const adjacencyList: number[][] = Array.from({ length: n }, () => []);
13  
14    // Create initial edges: 0->1, 1->2, ..., (n-2)->(n-1)
15    for (let i = 0; i < n - 1; i++) {
16        adjacencyList[i].push(i + 1);
17    }
18  
19    /**
20     * Performs BFS to find the shortest distance from a starting node to node n-1
21     * 
22     * @param startNode - The starting node for BFS
23     * @returns The shortest distance to node n-1
24     */
25    const bfs = (startNode: number): number => {
26        // Initialize queue with the starting node
27        let queue: number[] = [startNode];
28      
29        // Track visited nodes to avoid cycles
30        const visited: boolean[] = Array(n).fill(false);
31        visited[startNode] = true;
32      
33        // BFS level by level
34        for (let distance = 0; ; distance++) {
35            const nextLevelQueue: number[] = [];
36          
37            // Process all nodes at the current level
38            for (const currentNode of queue) {
39                // Check if we've reached the destination
40                if (currentNode === n - 1) {
41                    return distance;
42                }
43              
44                // Explore all neighbors of the current node
45                for (const neighbor of adjacencyList[currentNode]) {
46                    if (!visited[neighbor]) {
47                        visited[neighbor] = true;
48                        nextLevelQueue.push(neighbor);
49                    }
50                }
51            }
52          
53            // Move to the next level
54            queue = nextLevelQueue;
55        }
56    };
57  
58    // Store results for each query
59    const results: number[] = [];
60  
61    // Process each query
62    for (const [fromNode, toNode] of queries) {
63        // Add the new edge to the graph
64        adjacencyList[fromNode].push(toNode);
65      
66        // Calculate and store the shortest distance after adding this edge
67        results.push(bfs(0));
68    }
69  
70    return results;
71}
72

Time and Space Complexity

Time Complexity: O(q Γ— (n + q))

For each query, we perform a BFS traversal:

  • BFS in the worst case visits all n nodes
  • After processing q queries, each node can have at most q + 1 outgoing edges (1 original edge plus up to q added edges)
  • Thus, the total number of edges after q queries is at most n - 1 + q
  • Each BFS takes O(V + E) = O(n + (n - 1 + q)) = O(n + q) time
  • Since we run BFS for each of the q queries, the total time complexity is O(q Γ— (n + q))

Space Complexity: O(n + q)

The space usage consists of:

  • Graph adjacency list g: Initially has n - 1 lists with 1 element each. After q queries, it can have up to n - 1 + q total edges stored, requiring O(n + q) space
  • BFS queue q: Can contain at most n nodes at any time, requiring O(n) space
  • Visited array vis: Always has exactly n boolean values, requiring O(n) space
  • Result array ans: Stores q results, requiring O(q) space

The overall space complexity is O(n + q) as the graph storage dominates after multiple queries are added.

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

Common Pitfalls

1. Incorrect Graph Initialization

A frequent mistake is incorrectly initializing the adjacency list, especially forgetting that city n-1 has no outgoing edges initially.

Incorrect:

# This creates n lists instead of n-1
adjacency_list = [[i + 1] for i in range(n)]  # Wrong! Index n-1 would point to n

Correct:

# City n-1 has no outgoing edges initially
adjacency_list = [[i + 1] for i in range(n - 1)]

2. BFS Early Termination Logic

The current implementation checks if we've reached the destination after dequeuing, which is correct. A common mistake is checking before adding to the queue, which can lead to incorrect distance calculations.

Incorrect:

for neighbor in adjacency_list[current_node]:
    if neighbor == n - 1:
        return distance + 1  # Wrong! This might not be the shortest path
    if not visited[neighbor]:
        visited[neighbor] = True
        queue.append(neighbor)

Correct:

# Check after dequeuing to ensure we're processing at the correct distance level
if current_node == n - 1:
    return distance

3. Handling Edge Cases

The code assumes we'll always find a path (which is valid given the problem constraints). However, a more robust implementation should handle the case where no path exists or when n <= 1.

Enhanced Solution:

def bfs(start: int) -> int:
    if n <= 1:
        return 0
    if start == n - 1:
        return 0
      
    queue = deque([start])
    visited = [False] * n
    visited[start] = True
    distance = 0
  
    while queue:  # Changed from while True
        for _ in range(len(queue)):
            current_node = queue.popleft()
            if current_node == n - 1:
                return distance
          
            # Handle nodes that might not have outgoing edges
            if current_node < len(adjacency_list):
                for neighbor in adjacency_list[current_node]:
                    if not visited[neighbor]:
                        visited[neighbor] = True
                        queue.append(neighbor)
        distance += 1
  
    return -1  # No path found (shouldn't happen in this problem)

4. Memory Optimization Consideration

While the current solution is correct, repeatedly creating a new visited array for each BFS call can be optimized by reusing a single array:

class Solution:
    def shortestDistanceAfterQueries(self, n: int, queries: List[List[int]]) -> List[int]:
        # Create reusable visited array
        self.visited = [False] * n
      
        def bfs(start: int) -> int:
            # Reset visited array instead of creating new one
            for i in range(n):
                self.visited[i] = False
            self.visited[start] = True
            # ... rest of BFS logic

This reduces memory allocation overhead when processing many queries.

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

Depth first search is equivalent to which of the tree traversal order?


Recommended Readings

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

Load More