Jc-alt logo
jc
data structures and algorithms

LeetCode: Graphs II Dijkstras

LeetCode: Graphs II Dijkstras
13 min read
#data structures and algorithms

Dijkstras Algorithm Intro

Graph Requirements

Weighted, Directed Graph

Output

Shortest path from one node to every other node in graph. Differs from prim's and kruskal's which result in minimum spanning trees

Video Animation

https://www.youtube.com/watch?v=_lHSawdgXpI

Pseudo Code

Time Complexity

Space Complexity

IRL Use Case

What is Dijkstras Algorithm

Graphs with non negative weights

Dijkstra's Algorithm / Greedy BFS with MinHeap: Dijkstra is a graph traversal algorithm for finding the shortest path from a single source to all other nodes in a weighted graph with non-negative edge weights. Key Concepts:

  1. BFS Analogy:
    • Similar to BFS layer expansion, but we expand nodes in order of minimum current distance
    • Instead of levels, we prioritize nodes by shortest accumulated distance
  2. Greedy Choice:
    • Always pick the next node with the smallest known distance (via MinHeap)
    • Guarantees that when a node is popped from the heap, its shortest distance is final
  3. Differences vs DFS/BFS:
    • DFS/BFS explores nodes without weights consideration (uniform cost or unweighted)
    • Dijkstra accounts for edge weights and ensures optimal distances incrementally
  4. Graph Representation:
    • Nodes: vertices of the graph
    • Edges: weighted directed edges (u, v, w)
    • Implicit adjacency list or matrix Result:
  • Shortest distances from source to all reachable nodes
  • Unreachable nodes can be detected if not visited during traversal

743. Network Delay Time ::1:: - Medium

Topics: Depth First Search, Breadth First Search, Graph, Heap (Priority Queue), Shortest Path

Intro

You are given a network of n nodes, labeled from 1 to n. You are also given times, a list of travel times as directed edges times[i] = (ui, vi, wi), where ui is the source node, vi is the target node, and wi is the time it takes for a signal to travel from source to target. We will send a signal from a given node k. Return the minimum time it takes for all the n nodes to receive the signal. If it is impossible for all the n nodes to receive the signal, return -1.

Example InputOutput
times = [[2,1,1],[2,3,1],[3,4,1]], n = 4, k = 22
times = [[1,2,1]], n = 2, k = 11
times = [[1,2,1]], n = 2, k = 2-1

Constraints:

1 ≤ k ≤ n ≤ 100

1 ≤ times.length ≤ 6000

times[i].length == 3

1 ≤ ui, vi ≤ n

ui != vi

0 ≤ wi ≤ 100

All the pairs (ui, vi) are unique. (i.e., no multiple edges.)

Abstraction

Given a graph, each node with 1 edges, determine how much time is needed to get from start node to target node.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: [Dijkstra's] BFS And MinHeap To Keep Shortest Path - Advanced Graphs/Advanced Graphs

    def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
        
        # Dijkstra's Algorithm (Single-Source Shortest Path):
        # We want the minimum time for a signal sent from node k
        # to reach all n nodes in a directed graph with non-negative weights.
        
        # Key Ideas:
        #   1. Model the network as a directed weighted graph.
        #   2. Use a min-heap to always expand the node with the smallest
        #      known signal arrival time.
        #   3. Maintain a dictionary of shortest known times to each node.
        #   4. If all nodes are reached, return the maximum of these times.
        #   5. If some node is unreachable, return -1.

        # Build adjacency list: graph[src] = [(dest, weight), ...]
        # tc: O(edge)
        graph = defaultdict(list)
        for u, v, w in times:
            graph[u].append((v, w))
        
        # Min-Heap:
        # Each entry is (time_to_reach_node, node)
        # sc: O(node)
        heap = [(0, k)]
        
        # Dictionary to track shortest known time to each node
        # Invariant: once a node is added here, its shortest time is finalized
        shortest_time = {}
        
        # tc: each node/edge O(e) processed via O(log n) heap operations O(e log n)
        while heap:

            # Pop node with smallest known arrival time
            time, node = heapq.heappop(heap)
            
            # Skip if this node already has a finalized shortest time
            # (We found a better path earlier)
            if node in shortest_time:
                continue
            
            # Record shortest time to reach this node
            shortest_time[node] = time
            
            # Explore Choices:
            # Relax all outgoing edges from this node
            for neighbor, wt in graph[node]:

                # If neighbor not finalized, push updated time candidate
                if neighbor not in shortest_time:
                    heapq.heappush(heap, (time + wt, neighbor))
        
        # After processing:
        # If not all nodes were reached, signal cannot reach everyone
        if len(shortest_time) != n:
            return -1
        
        # Result:
        # The total network delay is the maximum shortest arrival time
        res = max(shortest_time.values())

        # overall: tc O(e log n)
        # overall: sc O(n + e)     
        return res

1631. Path With Minimum Effort ::1:: - Medium

Topics: Array, Binary Search, Depth First Search, Breadth First Search, Union Find, Heap (Priority Queue), Matrix

Intro

You are a hiker preparing for an upcoming hike. You are given heights, a 2D array of size rows x columns, where heights[row][col] represents the height of cell (row, col). You are situated in the top-left cell, (0, 0), and you hope to travel to the bottom-right cell, (rows-1, columns-1) (i.e., 0-indexed). You can move up, down, left, or right, and you wish to find a route that requires the minimum effort. A route's effort is the maximum absolute difference in heights between two consecutive cells of the route. Return the minimum effort required to travel from the top- left cell to the bottom-right cell.

Example InputOutput
heights = [[1,2,2],[3,8,2],[5,3,5]]2
heights = [[1,2,3],[3,8,4],[5,3,5]]1

Constraints:

rows == heights.length

columns == heights[i].length

1 ≤ rows, columns ≤ 100

1 ≤ heights[i][j] ≤ 106

Abstraction

Given a graph, determine the route with minimal climbing.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: Dijkstra + MinHeap - Advanced Graphs/Advanced Graphs

    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        
        # Why Dijkstras + MinHeap?
        # Dijkstras finds shortest path from a source node to all other nodes
        # in a weighted graph with non-negative edge weights, our case
        
        # Cell -> Node
        # Effort (absolute height difference) -> Weight
        # MinHeap ensures we always explore next cell with minimal effort first
        # Guarantees the minimum maximum effort path to the bottom-right cell.
        
        # Note:
        # 1. Use MinHeap to track cells by minimal effort seen so far
        # 2. Effort to reach a cell -> max(absolute difference along path)
        # 3. Track visited/effort for each cell
        # 4. Pop cell with lowest effort from heap, update neighbors
        # 5. Stop when reaching bottom-right cell
        # Result -> path of lowest effort

        rows, cols = len(heights), len(heights[0])
        efforts = [[float('inf')] * cols for _ in range(rows)]
        efforts[0][0] = 0

        # (effort, row, col)
        min_heap = [(0, 0, 0)]
        directions = [(0,1),(1,0),(-1,0),(0,-1)]

        while min_heap:
            curr_effort, r, c = heappop(min_heap)

            # Reached target -> bottom right
            if r == rows - 1 and c == cols - 1:
                return curr_effort

            for dr, dc in directions:
                nr, nc = r + dr, c + dc
                if 0 <= nr < rows and 0 <= nc < cols:
                    next_effort = max(curr_effort, abs(heights[r][c] - heights[nr][nc]))
                    if next_effort < efforts[nr][nc]:
                        efforts[nr][nc] = next_effort
                        heappush(min_heap, (next_effort, nr, nc))

        # default, though problem guarantees a path exists
    
        # overall: time complexity O(R*C*log(R*C)) due to heap operations
        # overall: space complexity O(R*C) for effort tracking and heap
        return 0

778. Swim in Rising Water ::1:: - Hard

Topics: Array, Binary Search, Depth First Search, Breadth First Search, Union Find, Heap (Priority Queue), Matrix

Intro

You are given an n x n integer matrix grid where each value grid[i][j] represents the elevation at that point (i, j). It starts raining, and water gradually rises over time. At time t, the water level is t, meaning any cell with elevation less than equal to t is submerged or reachable. You can swim from a square to another 4-directionally adjacent square if and only if the elevation of both squares individually are at most t. You can swim infinite distances in zero time. Of course, you must stay within the boundaries of the grid during your swim. Return the minimum time until you can reach the
bottom right square (n - 1, n - 1) if you start at the top left square (0, 0).

Example InputOutput
grid = [[0,2],[1,3]]3
grid = [[0,1,2,3,4],[24,23,22,21,5],[12,13,14,15,16],[11,17,18,19,20],[10,9,8,7,6]]16

Constraints:

n == grid.length

n == grid[i].length

1 ≤ n ≤ 50

0 ≤ grid[i][j] < n2

Each value grid[i][j] is unique.

Abstraction

Given a graph, determine the time needed to traverse from top left to bottom right.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: Dijkstra + MinHeap - Advanced Graphs/Advanced Graphs

    def swimInWater(self, grid: List[List[int]]) -> int:
        
        # Why Dijkstras + MinHeap? 
        # Cell -> Node
        # Weight -> determined by max elevation along path so far
        # Dijkstras algo finds path from source (0,0) to target (n-1,n-1)
        # that minimizes the max elevation
        # MinHeap ensures we always expand the path with lowest current
        # maximum elevation
        # Guarantees minimum time t
        # Result -> path of minimum height
        
        # Note:
        # 1. We want min time t to reach (n-1, n-1)
        # 2. At any step, t = max elevation along the path
        # 3. Use min-heap to always expand the path with lowest t so far
        # 4. Track visited cells to avoid revisiting

        n = len(grid)
        visited = [[False] * n for _ in range(n)]
        
        # (time_so_far, row, col)
        min_heap = [(grid[0][0], 0, 0)]
        directions = [(0,1),(1,0),(-1,0),(0,-1)]

        while min_heap:
            t, r, c = heappop(min_heap)
            if visited[r][c]:
                continue
            visited[r][c] = True

            # Reached target cell
            if r == n - 1 and c == n - 1:
                return t

            for dr, dc in directions:
                nr, nc = r + dr, c + dc
                if 0 <= nr < n and 0 <= nc < n and not visited[nr][nc]:

                    # time to reach neighbor = max(current path t, neighbor elevation)                    
                    heappush(min_heap, (max(t, grid[nr][nc]), nr, nc))

        # default, problem guarantees a path exists
        
        # overall: time complexity O(n^2 * log(n^2)) due to heap operations
        # overall: space complexity O(n^2) for heap and visited
        return 0

787. Cheapest Flights Within K Stops ::1:: - Medium

Topics: Dynamic Programming, Depth First Search, Breadth First Search, Graph, Heap (Priority Queue), Shortest Path

Intro

There are n cities connected by some number of
flights. You are given an array flights where flights[i] = [fromi, toi, pricei] indicates that pricei. You are also given three integers src, dst, and k, return the cheapest price from src to dst with at most k stops. If there is no such route, return -1.

Example InputOutput
n = 4, flights = [[0,1,100],[1,2,100],[2,0,100],[1,3,600],[2,3,200]], src = 0, dst = 3, k = 1700
n = 3, flights = [[0,1,100],[1,2,100],[0,2,500]], src = 0, dst = 2, k = 1200

Constraints:

1 ≤ n ≤ 100

0 ≤ flights.length ≤ (n * (n-1) / 2)

flight[i].length == 3

0 ≤ fromi, toi < n

fromi != toi

1 ≤ pricei ≤ 104

There will not be any multiple flights between two cities.

0 ≤ src, dst, k < n

src != dst

Abstraction

Given flights, determine the cheapest flights under k stops.

Space & Time Complexity

SolutionTime ComplexitySpace ComplexityTime RemarkSpace Remark
BugError

Brute Force:

AspectTime ComplexitySpace ComplexityTime RemarksSpace Remarks

Find the Bug:

Solution 1: Modified Dijkstra via BFS + MinHeap - Advanced Graphs/Advanced Graphs

    def findCheapestPrice(self, n: int, flights: List[List[int]], src: int, dst: int, k: int) -> int:
        
        # Modified Disjkstras?
        # Modified for graphs with 'stops' constraint
        # Each node in heap is (stops_so_far, current_node, cost_so_far)
        # MinHeap guarantees we expand path with lowest cost so far
        # Only continue paths that respect the stops limit (<= k)
        
        # Build adjacency list
        adj={i:[] for i in range(n)}
        for u,v,w in flights:
            adj[u].append((v,w))

        # Initialize distance array and min-heap
        dist=[float('inf')]*n
        dist[src]=0
        q=[]
        
        # (stops_so_far, current_node, cost_so_far)
        heapq.heappush(q,(0, src, 0))

        # Process heap
        while q:
            stops,node,wei=heapq.heappop(q)

            # Skip if stops exceed limit
            if stops>k:
                continue

            # Explore neighbors 
            for nei,w in adj[node]:
                next_cost = cost + w

                # Only push if we improve distance
                if dist[nei]>next_cost and stops<=k:
                    dist[nei]=next_cost
                    heapq.heappush(q,((stops+1,nei,next_cost)))
        print(dist)

        # Check if destination reachable
        if dist[dst]==float('inf'):
            return -1

        res = dist[dst]

        # overall: time complexity O(E log N) in practice, E = # of edges
        # overall: space complexity O(N + E) for adjacency list and heap    
        return res