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 elementroads[i] = [ui, vi, timei]
indicates there is a road between intersectionui
and intersectionvi
that takestimei
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.
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:
-
We found a shorter path: If the path through
t
gives us a distance ofdist[t] + g[t][j]
that is less than the current known shortest distancedist[j]
, then we've discovered a better route. In this case, we updatedist[j]
to this new shorter distance and set the number of ways to reachj
equal to the number of ways to reacht
(since all shortest paths toj
must now go throught
). -
We found an equally short path: If the path through
t
gives us exactly the same distance as the current shortest distance toj
, then we've found an alternative shortest path. We don't update the distance, but we add the number of ways to reacht
to the number of ways to reachj
, because we can use any of the paths tot
and then continue toj
.
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]
, setg[u][v] = g[v][u] = t
(bidirectional roads)
Then we initialize three arrays:
dist[i]
: shortest distance from intersection 0 to intersectioni
, initially all infinity exceptdist[0] = 0
f[i]
: number of shortest paths from intersection 0 to intersectioni
, initially all 0 exceptf[0] = 1
vis[i]
: whether intersectioni
has been visited, initially allFalse
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
throught
, we updatedist[j]
and setf[j] = f[t]
because all previous paths are no longer shortest - If we find an equally short path to
j
throught
, we addf[t]
tof[j]
because we've found additional ways to reachj
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 EvaluatorExample 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)
- Node 1:
- 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
, sodist[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)
- Node 2:
- 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
, sodist[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:
- Direct path: 0→3
- 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 alln
nodes) - Updating distances to all neighbors takes
O(n)
time (checking alln
possible connections)
- Finding the minimum unvisited node takes
- 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.
Which of the following is a good use case for backtracking?
Recommended Readings
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Topological Sort Topological Order Prereq Breadth First Search Review problems graph_bfs Directed Graph Before we get started on topological order let's talk about directed graphs first A graph is directed when its edges have directions these edges are also called arcs Suppose that v w are vertices in a directed
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!