Taro Logo

Number of Ways to Arrive at Destination

Medium
Google logo
Google
1 view
Topics:
GraphsDynamic Programming

You are in a city that consists of n intersections numbered from 0 to n - 1 with bi-directional roads between some intersections. The inputs are generated such that you can reach any intersection from any other intersection and that there is at most one road between any two intersections.

You are given an integer n and a 2D integer array roads where roads[i] = [u<sub>i</sub>, v<sub>i</sub>, time<sub>i</sub>] means that there is a road between intersections u<sub>i</sub> and v<sub>i</sub> that takes time<sub>i</sub> minutes to travel. You want to know in how many ways you can travel from intersection 0 to intersection n - 1 in the shortest amount of time.

Return the number of ways you can arrive at your destination in the shortest amount of time. Since the answer may be large, return it modulo 10<sup>9</sup> + 7.

For example:

n = 7, roads = [[0,6,7],[0,1,2],[1,2,3],[1,3,3],[6,3,3],[3,5,1],[6,5,1],[2,5,1],[0,4,5],[4,6,2]]

In this case the output is 4. The shortest amount of time it takes to go from intersection 0 to intersection 6 is 7 minutes. The four ways to get there in 7 minutes are:

  • 0 -> 6
  • 0 -> 4 -> 6
  • 0 -> 1 -> 2 -> 5 -> 6
  • 0 -> 1 -> 3 -> 5 -> 6

Another example:

n = 2, roads = [[1,0,10]]

In this case, the output is 1 because There is only one way to go from intersection 0 to intersection 1, and it takes 10 minutes. How would you approach this problem and what would be the most efficient solution?

Solution


Naive Solution (Brute Force)

A brute-force approach to solve this problem would involve exploring all possible paths from the starting intersection (0) to the destination intersection (n-1) and keeping track of the shortest time taken to reach the destination. Then, we count the number of paths that achieve this shortest time.

  1. Explore all paths: Use Depth-First Search (DFS) or Breadth-First Search (BFS) to explore all possible paths from intersection 0 to intersection n-1.
  2. Calculate path time: For each path, calculate the total time taken to traverse it.
  3. Find shortest time: Determine the shortest time among all paths.
  4. Count paths: Count the number of paths that take exactly the shortest time.

This approach is highly inefficient because it explores all possible paths, which can grow exponentially with the number of intersections and roads.

Big O Runtime: O(V^E) where V is number of vertices and E is number of edges, due to exploring all possible paths. Big O Space Usage: O(V), due to the depth of the call stack in DFS or the size of the queue in BFS.

Optimal Solution (Dijkstra with Path Counting)

A more efficient solution involves using Dijkstra's algorithm to find the shortest time from the starting intersection to all other intersections. Simultaneously, we maintain a count of the number of shortest paths to each intersection.

  1. Initialization:
    • dist[i] = shortest time from intersection 0 to intersection i (initialized to infinity for all i except 0, which is initialized to 0).
    • ways[i] = number of shortest paths from intersection 0 to intersection i (initialized to 0 for all i except 0, which is initialized to 1).
  2. Dijkstra's Algorithm:
    • Use a priority queue to store intersections to visit, prioritized by the shortest time (dist[i]).
    • While the priority queue is not empty:
      • Get the intersection u with the smallest dist[u] from the priority queue.
      • For each neighbor v of u:
        • If dist[u] + time(u, v) < dist[v]:
          • Update dist[v] = dist[u] + time(u, v).
          • Update ways[v] = ways[u] (reset the path count, as we've found a shorter path).
          • Add v to the priority queue.
        • Else if dist[u] + time(u, v) == dist[v]:
          • Update ways[v] = (ways[v] + ways[u]) % (10^9 + 7) (add the number of paths to u to the number of paths to v).
  3. Result:
    • ways[n-1] contains the number of shortest paths from intersection 0 to intersection n-1.

Edge Cases:

  • If n is 1, return 1.
  • Ensure to handle modulo operations correctly to prevent integer overflow.
  • If the graph is disconnected, the algorithm will still work, but the result for unreachable nodes will be based on initial infinite distances, so special handling might be needed based on problem interpretation (though the problem statement guarantees connectivity).

Big O Runtime: O(E log V), where V is the number of intersections and E is the number of roads. This is due to Dijkstra's algorithm using a priority queue.

Big O Space Usage: O(V + E), where V is the number of intersections (for dist and ways arrays) and E is the number of roads (for the adjacency list representation of the graph).

import heapq

def count_paths(n: int, roads: list[list[int]]) -> int:
    adj = [[] for _ in range(n)]
    for u, v, time in roads:
        adj[u].append((v, time))
        adj[v].append((u, time))

    dist = [float('inf')] * n
    ways = [0] * n
    dist[0] = 0
    ways[0] = 1
    pq = [(0, 0)]  # (distance, node)
    MOD = 10**9 + 7

    while pq:
        d, u = heapq.heappop(pq)

        if d > dist[u]:
            continue

        for v, time in adj[u]:
            if dist[u] + time < dist[v]:
                dist[v] = dist[u] + time
                ways[v] = ways[u]
                heapq.heappush(pq, (dist[v], v))
            elif dist[u] + time == dist[v]:
                ways[v] = (ways[v] + ways[u]) % MOD

    return ways[n - 1]