Facebook Pixel

1976. Number of Ways to Arrive at Destination

Problem Description

You are given a city with n intersections numbered from 0 to n - 1. These intersections are connected by bi-directional roads. The road network is designed such that:

  • You can reach any intersection from any other intersection
  • There is at most one direct road between any two intersections

The input provides:

  • An integer n representing the number of intersections
  • A 2D array roads where each element roads[i] = [ui, vi, timei] indicates there is a road between intersection ui and intersection vi that takes timei minutes to travel

Your task is to find the number of different ways to travel from intersection 0 to intersection n - 1 using the shortest possible time.

Since there might be multiple paths that all take the minimum time to reach the destination, you need to count all such paths. The answer should be returned modulo 10^9 + 7 as it could be very large.

For example, if the shortest time to reach from intersection 0 to intersection n - 1 is 10 minutes, and there are 3 different paths that all take exactly 10 minutes, then the answer would be 3.

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

Intuition

The problem asks for two things: finding the shortest path and counting how many ways we can achieve that shortest path. This naturally points us toward Dijkstra's algorithm, which finds shortest paths in weighted graphs.

The key insight is that while finding the shortest distances to each intersection, we can simultaneously track the number of ways to reach each intersection with that shortest distance.

Think of it this way: when we're exploring the graph and find a path to intersection j through intersection t, two scenarios can occur:

  1. We found a shorter path: If the path through t gives us a distance of dist[t] + g[t][j] that is less than the current known shortest distance dist[j], then we've discovered a better route. In this case, we update dist[j] to this new shorter distance and set the number of ways to reach j equal to the number of ways to reach t (since all shortest paths to j must now go through t).

  2. We found an equally short path: If the path through t gives us exactly the same distance as the current shortest distance to j, then we've found an alternative shortest path. We don't update the distance, but we add the number of ways to reach t to the number of ways to reach j, because we can use any of the paths to t and then continue to j.

By maintaining an array f where f[i] represents the number of shortest paths from the starting point to intersection i, we can build up the count incrementally. We start with f[0] = 1 (there's exactly one way to be at the starting point - by starting there), and as we process each intersection using Dijkstra's algorithm, we update the counts based on the two scenarios above.

The algorithm ensures that when we process an intersection t, we've already found the shortest distance to it and counted all possible shortest paths to it. This way, when we use t to update other intersections, the count f[t] is final and accurate.

Learn more about Graph, Topological Sort, Dynamic Programming and Shortest Path patterns.

Solution Approach

The solution implements the naive Dijkstra algorithm with path counting. Let's walk through the implementation step by step:

1. Initialize the Graph and Data Structures

First, we create an adjacency matrix g where g[i][j] represents the travel time between intersections i and j:

  • Initialize all values to infinity (inf) to indicate no direct connection
  • Set g[0][0] = 0 (distance from start to itself is 0)
  • For each road [u, v, t], set g[u][v] = g[v][u] = t (bidirectional roads)

Then we initialize three arrays:

  • dist[i]: shortest distance from intersection 0 to intersection i, initially all infinity except dist[0] = 0
  • f[i]: number of shortest paths from intersection 0 to intersection i, initially all 0 except f[0] = 1
  • vis[i]: whether intersection i has been visited, initially all False

2. Dijkstra's Algorithm with Path Counting

The algorithm runs for n iterations, and in each iteration:

a) Find the unvisited intersection with minimum distance:

t = -1
for j in range(n):
    if not vis[j] and (t == -1 or dist[j] < dist[t]):
        t = j

This finds the unvisited intersection t that has the smallest distance from the start.

b) Mark this intersection as visited:

vis[t] = True

c) Update distances and path counts for neighbors: For each intersection j that hasn't been processed yet:

ne = dist[t] + g[t][j]  # new distance through t
if dist[j] > ne:
    dist[j] = ne
    f[j] = f[t]  # All shortest paths to j now go through t
elif dist[j] == ne:
    f[j] += f[t]  # Found additional shortest paths through t

The key logic here:

  • If we find a shorter path to j through t, we update dist[j] and set f[j] = f[t] because all previous paths are no longer shortest
  • If we find an equally short path to j through t, we add f[t] to f[j] because we've found additional ways to reach j in the shortest time

3. Return the Result

After processing all intersections, f[n-1] contains the number of shortest paths from intersection 0 to intersection n-1. We return this value modulo 10^9 + 7:

return f[-1] % mod

The time complexity is O(n²) due to the naive implementation of Dijkstra's algorithm, where we scan all vertices to find the minimum in each iteration. The space complexity is O(n²) for storing the adjacency matrix.

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: We have 4 intersections (n = 4) with the following roads:

  • Road from 0 to 1: 2 minutes
  • Road from 0 to 2: 5 minutes
  • Road from 1 to 2: 1 minute
  • Road from 1 to 3: 4 minutes
  • Road from 2 to 3: 1 minute

Our goal is to find the number of shortest paths from intersection 0 to intersection 3.

Step 1: Initialize

  • Create adjacency matrix g with road times
  • dist = [0, inf, inf, inf] (shortest distances)
  • f = [1, 0, 0, 0] (number of shortest paths)
  • vis = [False, False, False, False] (visited status)

Iteration 1:

  • Find unvisited node with minimum distance: node 0 (dist=0)
  • Mark node 0 as visited: vis[0] = True
  • Update neighbors:
    • Node 1: dist[1] = 0 + 2 = 2, f[1] = 1 (found first path to node 1)
    • Node 2: dist[2] = 0 + 5 = 5, f[2] = 1 (found first path to node 2)
  • State: dist = [0, 2, 5, inf], f = [1, 1, 1, 0]

Iteration 2:

  • Find unvisited node with minimum distance: node 1 (dist=2)
  • Mark node 1 as visited: vis[1] = True
  • Update neighbors:
    • Node 2: ne = 2 + 1 = 3 < 5, so dist[2] = 3, f[2] = 1 (found shorter path through node 1)
    • Node 3: dist[3] = 2 + 4 = 6, f[3] = 1 (found first path to node 3)
  • State: dist = [0, 2, 3, 6], f = [1, 1, 1, 1]

Iteration 3:

  • Find unvisited node with minimum distance: node 2 (dist=3)
  • Mark node 2 as visited: vis[2] = True
  • Update neighbors:
    • Node 0: Already visited, skip
    • Node 1: Already visited, skip
    • Node 3: ne = 3 + 1 = 4 < 6, so dist[3] = 4, f[3] = 1 (found shorter path through node 2)
  • State: dist = [0, 2, 3, 4], f = [1, 1, 1, 1]

Iteration 4:

  • Find unvisited node with minimum distance: node 3 (dist=4)
  • Mark node 3 as visited: vis[3] = True
  • All nodes visited, algorithm complete

Result: The shortest path from 0 to 3 takes 4 minutes, and there is 1 way to achieve this shortest time (path: 0→1→2→3).

Now let's modify the example to show multiple shortest paths. If we add a direct road from 0 to 3 with time 4:

After running the algorithm with this additional road:

  • When we process node 0, we'd set dist[3] = 4, f[3] = 1
  • When we process node 2 (with dist=3), we'd find ne = 3 + 1 = 4 == dist[3]
  • Since distances are equal, we'd update: f[3] = f[3] + f[2] = 1 + 1 = 2

Result: There would be 2 shortest paths from 0 to 3, both taking 4 minutes:

  1. Direct path: 0→3
  2. Path through nodes: 0→1→2→3

The algorithm correctly counts both paths by accumulating the path counts when finding equal-distance routes.

Solution Implementation

1class Solution:
2    def countPaths(self, n: int, roads: List[List[int]]) -> int:
3        # Initialize adjacency matrix with infinity for all pairs
4        adjacency_matrix = [[float('inf')] * n for _ in range(n)]
5      
6        # Build the adjacency matrix from the roads (undirected graph)
7        for start_node, end_node, time in roads:
8            adjacency_matrix[start_node][end_node] = time
9            adjacency_matrix[end_node][start_node] = time
10      
11        # Distance from a node to itself is 0
12        adjacency_matrix[0][0] = 0
13      
14        # Initialize distance array to track shortest distance from node 0
15        distances = [float('inf')] * n
16        distances[0] = 0
17      
18        # Initialize path count array to track number of shortest paths
19        path_counts = [0] * n
20        path_counts[0] = 1  # One way to reach the starting node
21      
22        # Track visited nodes for Dijkstra's algorithm
23        visited = [False] * n
24      
25        # Dijkstra's algorithm main loop - process all n nodes
26        for _ in range(n):
27            # Find the unvisited node with minimum distance
28            current_node = -1
29            for node in range(n):
30                if not visited[node] and (current_node == -1 or distances[node] < distances[current_node]):
31                    current_node = node
32          
33            # Mark current node as visited
34            visited[current_node] = True
35          
36            # Update distances and path counts for all neighbors
37            for neighbor in range(n):
38                if neighbor == current_node:
39                    continue
40                  
41                # Calculate new distance through current node
42                new_distance = distances[current_node] + adjacency_matrix[current_node][neighbor]
43              
44                if distances[neighbor] > new_distance:
45                    # Found a shorter path to neighbor
46                    distances[neighbor] = new_distance
47                    path_counts[neighbor] = path_counts[current_node]
48                elif distances[neighbor] == new_distance:
49                    # Found another shortest path to neighbor
50                    path_counts[neighbor] += path_counts[current_node]
51      
52        # Return the number of shortest paths to the last node, modulo 10^9 + 7
53        MOD = 10**9 + 7
54        return path_counts[-1] % MOD
55
1class Solution {
2    public int countPaths(int n, int[][] roads) {
3        // Constants for infinity value and modulo
4        final long INF = Long.MAX_VALUE / 2;
5        final int MOD = (int) 1e9 + 7;
6      
7        // Initialize adjacency matrix for graph representation
8        long[][] graph = new long[n][n];
9        for (long[] row : graph) {
10            Arrays.fill(row, INF);
11        }
12      
13        // Build the graph from roads (bidirectional edges)
14        for (int[] road : roads) {
15            int u = road[0];
16            int v = road[1];
17            int time = road[2];
18            graph[u][v] = time;
19            graph[v][u] = time;
20        }
21      
22        // Distance from node to itself is 0
23        graph[0][0] = 0;
24      
25        // Initialize distance array for shortest paths from source (node 0)
26        long[] distance = new long[n];
27        Arrays.fill(distance, INF);
28        distance[0] = 0;
29      
30        // Initialize count array for number of shortest paths
31        long[] pathCount = new long[n];
32        pathCount[0] = 1;  // One way to reach source from itself
33      
34        // Track visited nodes for Dijkstra's algorithm
35        boolean[] visited = new boolean[n];
36      
37        // Dijkstra's algorithm with path counting
38        for (int i = 0; i < n; i++) {
39            // Find unvisited node with minimum distance
40            int minNode = -1;
41            for (int j = 0; j < n; j++) {
42                if (!visited[j] && (minNode == -1 || distance[j] < distance[minNode])) {
43                    minNode = j;
44                }
45            }
46          
47            // Mark the selected node as visited
48            visited[minNode] = true;
49          
50            // Update distances and path counts for neighbors
51            for (int neighbor = 0; neighbor < n; neighbor++) {
52                if (neighbor == minNode) {
53                    continue;
54                }
55              
56                // Calculate new distance through current node
57                long newDistance = distance[minNode] + graph[minNode][neighbor];
58              
59                if (distance[neighbor] > newDistance) {
60                    // Found a shorter path, update distance and reset path count
61                    distance[neighbor] = newDistance;
62                    pathCount[neighbor] = pathCount[minNode];
63                } else if (distance[neighbor] == newDistance) {
64                    // Found another shortest path, add to path count
65                    pathCount[neighbor] = (pathCount[neighbor] + pathCount[minNode]) % MOD;
66                }
67            }
68        }
69      
70        // Return number of shortest paths to destination (node n-1)
71        return (int) pathCount[n - 1];
72    }
73}
74
1class Solution {
2public:
3    int countPaths(int n, vector<vector<int>>& roads) {
4        // Constants for infinity value and modulo
5        const long long INF = LLONG_MAX / 2;
6        const int MOD = 1e9 + 7;
7
8        // Initialize adjacency matrix with infinity values
9        vector<vector<long long>> adjacencyMatrix(n, vector<long long>(n, INF));
10      
11        // Build the graph from roads (undirected graph)
12        for (auto& road : roads) {
13            int from = road[0];
14            int to = road[1];
15            int time = road[2];
16            adjacencyMatrix[from][to] = time;
17            adjacencyMatrix[to][from] = time;
18        }
19      
20        // Distance from node to itself is 0
21        adjacencyMatrix[0][0] = 0;
22
23        // Initialize distance array for Dijkstra's algorithm
24        vector<long long> distance(n, INF);
25        distance[0] = 0;
26
27        // Initialize count array to track number of shortest paths
28        vector<long long> pathCount(n, 0);
29        pathCount[0] = 1;  // One way to reach the starting node
30
31        // Track visited nodes for Dijkstra's algorithm
32        vector<bool> visited(n, false);
33      
34        // Dijkstra's algorithm with path counting
35        for (int i = 0; i < n; ++i) {
36            // Find the unvisited node with minimum distance
37            int minNode = -1;
38            for (int j = 0; j < n; ++j) {
39                if (!visited[j] && (minNode == -1 || distance[j] < distance[minNode])) {
40                    minNode = j;
41                }
42            }
43          
44            // Mark the selected node as visited
45            visited[minNode] = true;
46          
47            // Update distances and path counts for neighboring nodes
48            for (int neighbor = 0; neighbor < n; ++neighbor) {
49                // Skip self-loops
50                if (neighbor == minNode) {
51                    continue;
52                }
53              
54                // Calculate new distance through current node
55                long long newDistance = distance[minNode] + adjacencyMatrix[minNode][neighbor];
56              
57                // If we found a shorter path, update distance and reset path count
58                if (distance[neighbor] > newDistance) {
59                    distance[neighbor] = newDistance;
60                    pathCount[neighbor] = pathCount[minNode];
61                } 
62                // If we found another path of the same shortest distance, add to path count
63                else if (distance[neighbor] == newDistance) {
64                    pathCount[neighbor] = (pathCount[neighbor] + pathCount[minNode]) % MOD;
65                }
66            }
67        }
68      
69        // Return the number of shortest paths to the last node
70        return static_cast<int>(pathCount[n - 1]);
71    }
72};
73
1/**
2 * Counts the number of shortest paths from node 0 to node n-1 in a weighted undirected graph
3 * @param n - Number of nodes in the graph (nodes are labeled from 0 to n-1)
4 * @param roads - Array of edges where each edge is [u, v, time] representing a bidirectional road
5 * @returns The number of shortest paths from node 0 to node n-1, modulo 10^9 + 7
6 */
7function countPaths(n: number, roads: number[][]): number {
8    const MOD: number = 1e9 + 7;
9  
10    // Initialize adjacency matrix with infinity for all edges
11    const adjacencyMatrix: number[][] = Array.from(
12        { length: n }, 
13        () => Array(n).fill(Infinity)
14    );
15  
16    // Build the adjacency matrix from the roads (bidirectional edges)
17    for (const [fromNode, toNode, travelTime] of roads) {
18        adjacencyMatrix[fromNode][toNode] = travelTime;
19        adjacencyMatrix[toNode][fromNode] = travelTime;
20    }
21  
22    // Distance from a node to itself is 0
23    adjacencyMatrix[0][0] = 0;
24
25    // Initialize shortest distances array (Dijkstra's algorithm)
26    const shortestDistances: number[] = Array(n).fill(Infinity);
27    shortestDistances[0] = 0; // Starting node has distance 0
28
29    // Initialize path count array
30    // pathCounts[i] represents the number of shortest paths from node 0 to node i
31    const pathCounts: number[] = Array(n).fill(0);
32    pathCounts[0] = 1; // There's exactly one path from start to itself
33
34    // Track visited nodes for Dijkstra's algorithm
35    const visited: boolean[] = Array(n).fill(false);
36  
37    // Dijkstra's algorithm with path counting
38    for (let iteration = 0; iteration < n; ++iteration) {
39        // Find the unvisited node with minimum distance
40        let currentNode: number = -1;
41        for (let node = 0; node < n; ++node) {
42            if (!visited[node] && 
43                (currentNode === -1 || shortestDistances[node] < shortestDistances[currentNode])) {
44                currentNode = node;
45            }
46        }
47      
48        // Mark current node as visited
49        visited[currentNode] = true;
50      
51        // Update distances and path counts for all neighbors
52        for (let neighbor = 0; neighbor < n; ++neighbor) {
53            // Skip if it's the same node
54            if (neighbor === currentNode) {
55                continue;
56            }
57          
58            // Calculate new distance through current node
59            const newDistance: number = shortestDistances[currentNode] + adjacencyMatrix[currentNode][neighbor];
60          
61            if (shortestDistances[neighbor] > newDistance) {
62                // Found a shorter path to neighbor
63                shortestDistances[neighbor] = newDistance;
64                pathCounts[neighbor] = pathCounts[currentNode];
65            } else if (shortestDistances[neighbor] === newDistance) {
66                // Found another shortest path to neighbor
67                pathCounts[neighbor] = (pathCounts[neighbor] + pathCounts[currentNode]) % MOD;
68            }
69        }
70    }
71  
72    // Return the number of shortest paths to the destination node (n-1)
73    return pathCounts[n - 1];
74}
75

Time and Space Complexity

Time Complexity: O(n^2)

The algorithm implements Dijkstra's shortest path algorithm with path counting. The main time complexity comes from:

  • The outer loop runs n iterations
  • In each iteration:
    • Finding the minimum unvisited node takes O(n) time (scanning through all n nodes)
    • Updating distances to all neighbors takes O(n) time (checking all n possible connections)
  • Total: n × (O(n) + O(n)) = O(n^2)

Space Complexity: O(n^2)

The space usage includes:

  • Adjacency matrix g: n × n array storing edge weights = O(n^2)
  • Distance array dist: stores shortest distance to each node = O(n)
  • Path count array f: stores number of shortest paths to each node = O(n)
  • Visited array vis: tracks processed nodes = O(n)
  • Total: O(n^2) + O(n) + O(n) + O(n) = O(n^2)

The quadratic space complexity is dominated by the adjacency matrix representation of the graph.

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

Common Pitfalls

1. Forgetting to Apply Modulo During Path Count Updates

The most critical pitfall in this solution is only applying the modulo operation at the very end when returning the result. When dealing with path counting problems where the answer can be very large, intermediate calculations can cause integer overflow even in Python (though Python handles big integers automatically, it's inefficient).

The Problem:

elif distances[neighbor] == new_distance:
    # Found another shortest path to neighbor
    path_counts[neighbor] += path_counts[current_node]  # Can grow very large!

The Solution: Apply modulo during the accumulation to keep numbers manageable:

MOD = 10**9 + 7
# ... in the update logic:
elif distances[neighbor] == new_distance:
    path_counts[neighbor] = (path_counts[neighbor] + path_counts[current_node]) % MOD

2. Incorrect Adjacency Matrix Initialization

Another common mistake is incorrectly initializing the diagonal of the adjacency matrix. The code sets adjacency_matrix[0][0] = 0, but it should set all diagonal elements to 0:

The Problem:

adjacency_matrix[0][0] = 0  # Only sets distance from node 0 to itself

The Solution:

for i in range(n):
    adjacency_matrix[i][i] = 0  # Distance from any node to itself is 0

3. Not Handling Edge Cases

The solution doesn't explicitly handle edge cases like:

  • When n = 1 (start and destination are the same)
  • When there's no path from node 0 to node n-1 (though the problem guarantees connectivity)

The Solution: Add edge case handling:

if n == 1:
    return 1  # Only one way when start equals destination

# After Dijkstra's algorithm:
if distances[n-1] == float('inf'):
    return 0  # No path exists

4. Inefficient Node Selection in Dijkstra's

The current implementation uses O(n) time to find the minimum distance node in each iteration, leading to O(n²) overall complexity. For sparse graphs, using a priority queue would be more efficient.

The Solution using heapq:

import heapq

def countPaths(self, n: int, roads: List[List[int]]) -> int:
    MOD = 10**9 + 7
  
    # Build adjacency list instead of matrix
    graph = [[] for _ in range(n)]
    for u, v, time in roads:
        graph[u].append((v, time))
        graph[v].append((u, time))
  
    distances = [float('inf')] * n
    distances[0] = 0
    path_counts = [0] * n
    path_counts[0] = 1
  
    # Priority queue: (distance, node)
    pq = [(0, 0)]
  
    while pq:
        curr_dist, node = heapq.heappop(pq)
      
        if curr_dist > distances[node]:
            continue
      
        for neighbor, time in graph[node]:
            new_dist = curr_dist + time
          
            if distances[neighbor] > new_dist:
                distances[neighbor] = new_dist
                path_counts[neighbor] = path_counts[node]
                heapq.heappush(pq, (new_dist, neighbor))
            elif distances[neighbor] == new_dist:
                path_counts[neighbor] = (path_counts[neighbor] + path_counts[node]) % MOD
  
    return path_counts[n-1] % MOD

This optimized version runs in O((E + V) log V) time complexity, which is much better for sparse graphs.

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

Which of the following is a good use case for backtracking?


Recommended Readings

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

Load More