3 minute read

Problem Statement

problem

Brute Force [TLE]

class Solution:
    def dijkstra(self, graph, start):
        heap = [(0, start)]
        shortest_time = {node: float('inf') for node in graph}
        shortest_time[start] = 0

        while heap:
            curr_dist, node = heapq.heappop(heap)

            if curr_dist > shortest_time[node]:
                continue

            for nei, w in graph[node]:
                dist = curr_dist + w
                if dist < shortest_time[nei]:
                    shortest_time[nei] = dist
                    heapq.heappush(heap, (dist, nei))

        return shortest_time

    def backtrack(self, graph, limit_time, node, n, visited, time, nums_way):
        if node == n - 1:
            if time == limit_time:
                nums_way[0] += 1
            return

        for x in graph[node]:
            nei, t = x
            if nei not in visited:
                visited.add(nei)
                self.backtrack(graph, limit_time, nei, n, visited, time + t, nums_way)
                visited.remove(nei)


    def countPaths(self, n: int, roads: List[List[int]]) -> int:
        graph = {i:[] for i in range(n)}
        for u, v, time in roads:
            graph[u].append([v, time])
            graph[v].append([u, time])

        shortest_time = self.dijkstra(graph, 0)
        limit_time = shortest_time[n - 1]
        num_ways = [0]
        for i in range(n):
            self.backtrack(graph, limit_time, i, n, set(), time, num_ways)

        return num_ways[0]

Try to optimize algorithm but still get TLE.

import heapq
from typing import List

class Solution:
    def dijkstra(self, graph, start):
        heap = [(0, start)]
        shortest_time = {node: float('inf') for node in graph}
        shortest_time[start] = 0

        while heap:
            curr_dist, node = heapq.heappop(heap)

            if curr_dist > shortest_time[node]:
                continue

            for nei, w in graph[node]:
                new_dist = curr_dist + w
                if new_dist < shortest_time[nei]:
                    shortest_time[nei] = new_dist
                    heapq.heappush(heap, (new_dist, nei))

        return shortest_time

    def backtrack(self, graph, limit_time, node, n, visited, time, num_ways, shortest_time):
        if time > limit_time:  # Prune unnecessary paths
            return
        if node == n - 1:
            if time == limit_time:
                num_ways[0] += 1
            return

        for nei, t in graph[node]:
            if nei not in visited and time + t <= shortest_time[nei]:  # Ensure we're following the shortest path
                visited.add(nei)
                self.backtrack(graph, limit_time, nei, n, visited, time + t, num_ways, shortest_time)
                visited.remove(nei)

    def countPaths(self, n: int, roads: List[List[int]]) -> int:
        graph = {i: [] for i in range(n)}
        for u, v, time in roads:
            graph[u].append((v, time))
            graph[v].append((u, time))

        shortest_time = self.dijkstra(graph, 0)
        limit_time = shortest_time[n - 1]

        num_ways = [0]
        self.backtrack(graph, limit_time, 0, n, set([0]), 0, num_ways, shortest_time)  # Start from node 0

        return num_ways[0]

Editorial

Approach 1: Dijkstra’s Algorithm

class Solution:
    def countPaths(self, n: int, roads: list[list[int]]) -> int:
        MOD = 1_000_000_007

        # Build adjacency list
        graph = [[] for _ in range(n)]
        for start_node, end_node, travel_time in roads:
            graph[start_node].append((end_node, travel_time))
            graph[end_node].append((start_node, travel_time))

        # Min-Heap (priority queue) for Dijkstra
        min_heap = [(0, 0)]  # (time, node)
        # Store shortest time to each node
        shortest_time = [float("inf")] * n
        # Number of ways to reach each node in shortest time
        path_count = [0] * n

        shortest_time[0] = 0  # Distance to source is 0
        path_count[0] = 1  # 1 way to reach node 0

        while min_heap:
            curr_time, curr_node = heapq.heappop(min_heap)
            # Skip outdated distances
            if curr_time > shortest_time[curr_node]:
                continue

            for neighbor_node, road_time in graph[curr_node]:
                # Found a new shortest path → Update shortest time and reset path count
                if curr_time + road_time < shortest_time[neighbor_node]:
                    shortest_time[neighbor_node] = curr_time + road_time
                    path_count[neighbor_node] = path_count[curr_node]
                    heapq.heappush(
                        min_heap, (shortest_time[neighbor_node], neighbor_node)
                    )

                # Found another way with the same shortest time → Add to path count
                elif curr_time + road_time == shortest_time[neighbor_node]:
                    path_count[neighbor_node] = (
                        path_count[neighbor_node] + path_count[curr_node]
                    ) % MOD

        return path_count[n - 1]

Approach 2: Floyd-Warshall algorithm

class Solution:
    MOD = 1_000_000_007

    def countPaths(self, n: int, roads: list[list[int]]) -> int:
        # dp[src][dest][0] stores the minimum time between src and dest
        # dp[src][dest][1] stores the number of ways to reach dest from src
        # with the minimum time
        dp = [[[0, 0] for _ in range(n)] for _ in range(n)]

        # Initialize the dp table
        for src in range(n):
            for dest in range(n):
                if src != dest:
                    # Set a large initial time
                    dp[src][dest][0] = int(1e12)
                    # No paths yet
                    dp[src][dest][1] = 0
                else:
                    # Distance from a node to itself is 0
                    dp[src][dest][0] = 0
                    # Only one trivial way (staying at the node)
                    dp[src][dest][1] = 1

        # Initialize direct roads from the input
        for start_node, end_node, travel_time in roads:
            dp[start_node][end_node][0] = travel_time
            dp[end_node][start_node][0] = travel_time
            # There is one direct path
            dp[start_node][end_node][1] = 1
            # Since the roads are bidirectional
            dp[end_node][start_node][1] = 1

        # Apply the Floyd-Warshall algorithm to compute shortest paths
        # Intermediate node
        for mid in range(n):
            # Starting node
            for src in range(n):
                # Destination node
                for dest in range(n):
                    # Avoid self-loops
                    if src != mid and dest != mid:
                        new_time = dp[src][mid][0] + dp[mid][dest][0]

                        if new_time < dp[src][dest][0]:
                            # Found a shorter path
                            dp[src][dest][0] = new_time
                            dp[src][dest][1] = (
                                dp[src][mid][1] * dp[mid][dest][1]
                            ) % self.MOD
                        elif new_time == dp[src][dest][0]:

                            # Another way to achieve the same shortest time
                            dp[src][dest][1] = (
                                dp[src][dest][1]
                                + dp[src][mid][1] * dp[mid][dest][1]
                            ) % self.MOD

        # Return the number of shortest paths from node (n-1) to node 0
        return dp[n - 1][0][1]