Python Algorithms & Data Structures track

Lesson 24 · Part 7: Advanced Graphs

Dijkstra

Priority-queue shortest paths and the limits of negative weights

Part 7: Advanced Graphs3 min readFree lesson

Part 07 — Advanced Graphs | Previous: 23_Union_Find | Next: 25_Floyd_Warshall


Introduction

Dijkstra's algorithm is a representative greedy algorithm that solves the Single-Source Shortest Path problem. In a weighted Directed/Undirected graph, it finds the shortest distance from the starting node to all other nodes.

  • Condition: All edge weights ≥ 0
  • Time Complexity: O((V + E) log V) — using a priority queue (heap)
  • Space Complexity: O(V + E)

Since it is the most frequently appearing shortest path algorithm in coding tests, you should master it completely.


Learning Goals

  1. Understand and implement Dijkstra's algorithm (Greedy + Priority Queue) directly.
  2. Add path reconstruction to output the actual path.
  3. Understand why it fails with negative weights, and apply the Bellman-Ford algorithm as an alternative.

Key Concepts

Algorithm Operation Principle

1. Initialize dist[src] = 0, and the rest dist[v] = INF
2. Insert (0, src) into the min-heap
3. Until the heap is empty:
   a. (d, u) = heappop(heap)
b. If d > dist[u], skip (lazy deletion)
c. For adjacent nodes v of u:
      new_dist = d + weight(u, v)
      if new_dist < dist[v]:
          dist[v] = new_dist
          heappush(heap, (new_dist, v))

Key Invariant: The shortest distance of a node popped from the heap is fixed. This is only guaranteed when all edge weights are 0 or greater.


Complexity Comparison

Implementation Method Time Complexity Space Complexity Note
Array (naive) O(V²) O(V) Favorable for dense graphs
Binary Heap (heapq) O((V+E) log V) O(V+E) Standard for sparse graphs
Fibonacci Heap O(E + V log V) O(V+E) Theoretically optimal, complex implementation

Example Graph (Step-by-Step Trace)

        2         5
   (1)------(2)------(4)
    |      / |        |
   1|    3/  |4       |6
    |  /     |        |
   (3)------(5)------(6)
        1         2

Departure: Node 1
Step Confirmed Nodes dist[1] dist[2] dist[3] dist[4] dist[5] dist[6]
Initial — 0 INF INF INF INF INF
1 1 0 2 1 INF INF INF
2 3 0 2 1 INF 2 INF
3 2 0 2 1 7 2 INF
4 5 0 2 1 7 2 4
5 6 0 2 1 7 2 4
6 4 0 2 1 7 2 4

Final Shortest Distances: 1→1=0, 1→2=2, 1→3=1, 1→4=7, 1→5=2, 1→6=4


Relaxation Operation

#Core operation: update dist[v] to a smaller value
if dist[u] + weight(u, v) < dist[v]:
    dist[v] = dist[u] + weight(u, v)
    heappush(heap, (dist[v], v))

Lazy Deletion

Python heapq cannot delete elements. Therefore, if d > dist[u] is popped from the heap, it is a node that has already been processed and is skipped.

d, u = heapq.heappop(heap)
if d > dist[u]:   #Already processed via a shorter path
    continue

Example

File Description Key Concept
src/01_dijkstra_basic.py Basic Dijkstra Implementation heapq, dist array
src/02_dijkstra_path.py Including Path Restoration prev array, backtracking
src/03_dijkstra_negative.py Proof of failure with negative weights + Bellman-Ford Bellman-Ford O(VE)
src/04_dijkstra_applications.py K-th shortest path, state graph Modified Dijkstra

Common Mistakes

  1. Using negative weights: Dijkstra gives incorrect answers with negative edges. If there are negatives, use Bellman-Ford (O(VE)) or SPFA.

  2. Omitting lazy deletion: If you forget to check if d > dist[u]: continue after popping from the heap, duplicate handling can cause errors.

  3. 0-indexed vs 1-indexed confusion: Problems on Baekjoon are usually 1-indexed. Declare using dist = [INF] * (V + 1).

  4. Incorrect INF value setting: Use float('inf') or 10**18. 10**9 can cause overflow (not a problem in Python but can cause logic errors) when multiplied by the number of nodes.

  5. Single-direction input in an undirected graph: For undirected graphs, you must add both graph[u].append((v, w)) and graph[v].append((u, w)).


Summary

Dijkstra Algorithm Summary
━━━━━━━━━━━━━━━━━━━━━
Applicable Conditions: Graph with non-negative weights
Time Complexity: O((V + E) log V)
Space Complexity: O(V + E)
Key Data Structure: Min Heap (heapq)
Key Operations: Relaxation + Lazy Deletion
Path Reconstruction: Backtracking with prev[] array
Negative Edges: Use Bellman-Ford

Try It Yourself

  1. Track Dijkstra by hand starting from node 1 in the graph below.

    1 --3-- 2 --1-- 3
    |               |
    5               2
    |               |
    4 ------4------ 5
    
  2. Modify src/01_dijkstra_basic.py to make Dijkstra work on a reversed graph.

  3. Solve Baekjoon problem 1753 using src/02_dijkstra_path.py.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/01_dijkstra_basic.pyPython
"""Unit 24 — Dijkstra: Basic Implementation
Topic : Dijkstra's Algorithm (Basic)
Time  : O((V + E) log V)
Space : O(V + E)

Description:
    Dijkstra based on a priority queue using heapq.
    Finds the shortest distance from a single starting point to all nodes.
    Works only on graphs without negative weights."""

import heapq
import sys

INF = sys.maxsize


def dijkstra(graph: list[list[tuple[int, int]]], src: int) -> list[int]:
    """Returns the single-source shortest path.

    Args:
        graph: adjacency list in the form graph[u] = [(v, weight), ...]
        src  : starting node (0-indexed)

    Returns:
        dist[v] = shortest distance from src to v (INF if unreachable)

    Time : O((V + E) log V)
    Space: O(V + E)"""
    n = len(graph)
    dist = [INF] * n
    dist[src] = 0

    #Insert into the heap in the form of (distance, node)
    heap: list[tuple[int, int]] = [(0, src)]

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

        #Lazy Deletion: Skip nodes that have already been processed with a shorter distance
        if d > dist[u]:
            continue

        for v, weight in graph[u]:
            new_dist = dist[u] + weight
            if new_dist < dist[v]:
                dist[v] = new_dist
                heapq.heappush(heap, (new_dist, v))

    return dist


def build_graph(n: int, edges: list[tuple[int, int, int]],
                directed: bool = False) -> list[list[tuple[int, int]]]:
    """Creates an adjacency list from a list of edges.

    Args:
        n       : number of nodes (0-indexed: 0 ~ n-1)
        edges   : list of (u, v, weight)
        directed: True for directed graph, False for undirected graph

    Time : O(E)
    Space: O(V + E)"""
    graph: list[list[tuple[int, int]]] = [[] for _ in range(n)]
    for u, v, w in edges:
        graph[u].append((v, w))
        if not directed:
            graph[v].append((u, w))
    return graph


def main() -> None:
    #── Example 1: Graph from lecture text (1-indexed → 0-indexed conversion) ──
    #Nodes: 1~6 → 0~5
    #Edges: (u, v, w) (undirected)
    edges_ex1 = [
        (0, 1, 2),   # 1-2
        (0, 2, 1),   # 1-3
        (1, 2, 3),   # 2-3
        (1, 3, 5),   # 2-4
        (1, 4, 4),   # 2-5
        (2, 4, 1),   # 3-5
        (3, 5, 6),   # 4-6
        (4, 5, 2),   # 5-6
    ]
    graph1 = build_graph(6, edges_ex1, directed=False)
    dist1 = dijkstra(graph1, src=0)

    print("=" * 50)
    print("Example 1: Graph from lecture text (Start: Node 1)")
    print("=" * 50)
    for i, d in enumerate(dist1):
        label = str(d) if d < INF else "Unreachable"
        print(f"Node 1 → Node {i + 1}: {label}")

    #── Example 2: Directed graph ──
    print()
    print("=" * 50)
    print("Example 2: Directed graph (Start: Node 0)")
    print("=" * 50)
    edges_ex2 = [
        (0, 1, 4),
        (0, 2, 1),
        (2, 1, 2),
        (1, 3, 1),
        (2, 3, 5),
    ]
    graph2 = build_graph(4, edges_ex2, directed=True)
    dist2 = dijkstra(graph2, src=0)
    node_names = ["A", "B", "C", "D"]
    for i, d in enumerate(dist2):
        label = str(d) if d < INF else "Unreachable"
        print(f"  A → {node_names[i]}: {label}")

    #── Example 3: Including unreachable nodes ──
    print()
    print("=" * 50)
    print("Example 3: Including unreachable nodes")
    print("=" * 50)
    edges_ex3 = [(0, 1, 3), (1, 2, 2)]
    graph3 = build_graph(5, edges_ex3, directed=True)  #Nodes 3 and 4 are unreachable
    dist3 = dijkstra(graph3, src=0)
    for i, d in enumerate(dist3):
        label = str(d) if d < INF else "INF (unreachable)"
        print(f"  0 → {i}: {label}")


if __name__ == "__main__":
    main()
src/02_dijkstra_path.pyPython
"""Chapter 24 — Dijkstra: Path Reconstruction
Topic : Dijkstra with Path Reconstruction
Time  : O((V + E) log V)
Space : O(V + E)

Explanation:
    By managing the prev[] array together, we can backtrack the shortest path.
    Whenever dist[v] is updated, record prev[v] = u.
    By following prev from the destination back to the source, the shortest path can be obtained."""

import heapq
import sys

INF = sys.maxsize


def dijkstra_with_path(
    graph: list[list[tuple[int, int]]],
    src: int,
) -> tuple[list[int], list[int]]:
    """Returns the shortest distance + path reconstruction information.

    Args:
        graph: graph[u] = [(v, weight), ...] adjacency list
        src  : source node (0-indexed)

    Returns:
        (dist, prev)
        dist[v] : shortest distance from src to v
        prev[v] : previous node of v in the shortest path (-1 if none)

    Time : O((V + E) log V)
    Space: O(V + E)"""
    n = len(graph)
    dist = [INF] * n
    prev = [-1] * n          #For path restoration: prev[v] = node just before v
    dist[src] = 0

    heap: list[tuple[int, int]] = [(0, src)]

    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:      # lazy deletion
            continue

        for v, weight in graph[u]:
            new_dist = dist[u] + weight
            if new_dist < dist[v]:
                dist[v] = new_dist
                prev[v] = u  #← Record path
                heapq.heappush(heap, (new_dist, v))

    return dist, prev


def reconstruct_path(prev: list[int], src: int, dst: int) -> list[int]:
    """Backtracks the prev array to return the path from src to dst.

    Args:
        prev: prev array returned by dijkstra_with_path
        src : source node
        dst : destination node

    Returns:
        List of nodes in the shortest path from src to dst (empty list if unreachable)

    Time : O(V)  (path length ≤ V)
    Space: O(V)"""
    if prev[dst] == -1 and dst != src:
        return []   #Unreachable

    path: list[int] = []
    cur = dst
    while cur != -1:
        path.append(cur)
        cur = prev[cur]

    path.reverse()
    return path


def main() -> None:
    #── Example graph (undirected) ──
    #Nodes: 0=A, 1=B, 2=C, 3=D, 4=E
    #
    #   A --4-- B --2-- D
    #   |      /        |
    #   2    1          3
    #   |  /            |
    #   C ------3------ E
    #
    n = 5
    edges = [
        (0, 1, 4),  # A-B
        (0, 2, 2),  # A-C
        (1, 2, 1),  # B-C
        (1, 3, 2),  # B-D
        (2, 4, 3),  # C-E
        (3, 4, 3),  # D-E
    ]

    graph: list[list[tuple[int, int]]] = [[] for _ in range(n)]
    for u, v, w in edges:
        graph[u].append((v, w))
        graph[v].append((u, w))

    names = ["A", "B", "C", "D", "E"]
    src = 0   #Start: A

    dist, prev = dijkstra_with_path(graph, src)

    print("=" * 55)
    print(f"Start point: {names[src]}")
    print("=" * 55)
    print(f"{'Destination':>5} {'Shortest Distance':>8}  {'Path'}")
    print("-" * 55)

    for dst in range(n):
        d = dist[dst] if dist[dst] < INF else "INF"
        path = reconstruct_path(prev, src, dst)
        path_str = " → ".join(names[p] for p in path) if path else "Unreachable"
        print(f"  {names[src]} → {names[dst]}  {str(d):>6}    {path_str}")

    #── Detailed output for a specific path ──
    print()
    print("=" * 55)
    print("A → E Detailed Path")
    print("=" * 55)
    path_ae = reconstruct_path(prev, 0, 4)
    print(f"Path  : {' → '.join(names[p] for p in path_ae)}")
    print(f"Distance  : {dist[4]}")
    print()

    #── Unreachable cases ──
    print("=" * 55)
    print("Unreachable test (including isolated node)")
    print("=" * 55)
    graph2: list[list[tuple[int, int]]] = [[] for _ in range(3)]
    graph2[0].append((1, 5))  #Node 2 is isolated
    dist2, prev2 = dijkstra_with_path(graph2, 0)
    path2 = reconstruct_path(prev2, 0, 2)
    print(f"0 → 2: Distance={dist2[2]}, Path={path2 if path2 else 'Unreachable'}")


if __name__ == "__main__":
    main()
src/03_dijkstra_negative.pyPython
"""Chapter 24 — Dijkstra: Negative Edge Problem + Bellman-Ford Alternative
Topic : Negative Weights — Why Dijkstra Fails + Bellman-Ford
Time  : Dijkstra O((V+E) log V) / Bellman-Ford O(VE)
Space : O(V + E)

Description:
    Confirm directly why Dijkstra produces wrong answers when negative edges exist,
    and obtain the correct answer using the Bellman-Ford algorithm.
    Includes methods for detecting negative cycles."""

import heapq
import sys

INF = sys.maxsize


# ──────────────────────────────────────────────
#1. Dijkstra (Incorrect for negative edges)
# ──────────────────────────────────────────────

def dijkstra(graph: list[list[tuple[int, int]]], src: int) -> list[int]:
    """Basic Dijkstra (does not support negative edges).

    Time : O((V + E) log V)
    Space: O(V + E)"""
    n = len(graph)
    dist = [INF] * n
    dist[src] = 0
    heap: list[tuple[int, int]] = [(0, src)]

    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:
            continue
        for v, w in graph[u]:
            nd = dist[u] + w
            if nd < dist[v]:
                dist[v] = nd
                heapq.heappush(heap, (nd, v))
    return dist


# ──────────────────────────────────────────────
#2. Bellman-Ford (Supports negative edges, detects negative cycles)
# ──────────────────────────────────────────────

def bellman_ford(
    n: int,
    edges: list[tuple[int, int, int]],
    src: int,
) -> tuple[list[int], bool]:
    """Bellman-Ford Algorithm.

    Args:
        n    : Number of nodes
        edges: List of directed edges (u, v, w)
        src  : Starting node

    Returns:
        (dist, has_negative_cycle)
        dist[v]            : Shortest distance from src to v
        has_negative_cycle : True if a negative cycle exists

    Time : O(V * E)  — Repeat relaxation V-1 times + 1 cycle check
    Space: O(V)"""
    dist = [INF] * n
    dist[src] = 0

    #Repeat V-1 times: The shortest path has V-1 or fewer edges
    for _ in range(n - 1):
        updated = False
        for u, v, w in edges:
            if dist[u] != INF and dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                updated = True
        if not updated:   #Early termination optimization
            break

    #If relaxation occurs in the V-th iteration, a negative cycle exists
    has_negative_cycle = False
    for u, v, w in edges:
        if dist[u] != INF and dist[u] + w < dist[v]:
            has_negative_cycle = True
            break

    return dist, has_negative_cycle


# ──────────────────────────────────────────────
#3. Experiment
# ──────────────────────────────────────────────

def main() -> None:
    #── Case 1: Graph with negative edges ──
    # 0 ─4─▶ 1 ─(-3)─▶ 2
    # 0 ─2─▶ 2
    #
    #Actual shortest: 0→2 = 0+4+(-3) = 1
    #Dijkstra: First fixes 0→2 (2), so incorrect!
    print("=" * 55)
    print("Case 1: Graph with negative edges")
    print("  0 --4→ 1 --(-3)→ 2")
    print("  0 --2→ 2")
    print("=" * 55)

    edges1 = [(0, 1, 4), (1, 2, -3), (0, 2, 2)]
    n1 = 3

    #Graph for Dijkstra
    g_dijk = [[] for _ in range(n1)]
    for u, v, w in edges1:
        g_dijk[u].append((v, w))

    dist_dijk = dijkstra(g_dijk, 0)
    dist_bf, neg_cycle = bellman_ford(n1, edges1, 0)

    print(f"{'Node':>5} {'Dijkstra':>12} {'Bellman-Ford':>12} {'Correct?'}")
    print("-" * 50)
    for i in range(n1):
        d = dist_dijk[i] if dist_dijk[i] < INF else "INF"
        b = dist_bf[i] if dist_bf[i] < INF else "INF"
        correct = "✓" if d == b else "✗ Wrong!"
        print(f"  0 → {i}  {str(d):>10}  {str(b):>10}  {correct}")

    print(f"Negative cycle: {'Exists' if neg_cycle else 'None'}")

    #── Case 2: Negative cycle ──
    print()
    print("=" * 55)
    print("Case 2: Negative cycle graph")
    print("0 --1→ 1 --(-2)→ 2 --1→ 1  (Cycle: 1→2→1)")
    print("=" * 55)

    edges2 = [(0, 1, 1), (1, 2, -2), (2, 1, 1)]
    dist2, neg2 = bellman_ford(3, edges2, 0)
    print(f"Negative cycle detection: {'Exists → shortest distance undefined' if neg2 else 'None'}")

    #── Case 3: Normal (No negative edges) ──
    print()
    print("=" * 55)
    print("Case 3: Normal graph (No negatives) — both algorithms identical")
    print("=" * 55)

    edges3 = [(0, 1, 3), (0, 2, 1), (1, 3, 2), (2, 1, 1), (2, 3, 5)]
    n3 = 4
    g3 = [[] for _ in range(n3)]
    for u, v, w in edges3:
        g3[u].append((v, w))

    dist3_d = dijkstra(g3, 0)
    dist3_b, _ = bellman_ford(n3, edges3, 0)

    for i in range(n3):
        dd = dist3_d[i] if dist3_d[i] < INF else "INF"
        db = dist3_b[i] if dist3_b[i] < INF else "INF"
        print(f"0 → {i}: Dijkstra={dd}, Bellman-Ford={db}")

    #── Algorithm comparison summary ──
    print()
    print("=" * 55)
    print("Algorithm selection guide")
    print("=" * 55)
    print("No negative edges → Dijkstra  O((V+E) log V)")
    print("Negative edge exists     → Bellman-Ford   O(VE)")
    print("Detect negative cycle   → Bellman-Ford check on the V-th iteration")
    print("All-pairs shortest path     → Floyd-Warshall O(V³)")


if __name__ == "__main__":
    main()
src/04_dijkstra_applications.pyPython
"""Unit 24 — Dijkstra: Applications (K-th Shortest Path, State Graph)
Topic : Modified Dijkstra — K-th Shortest Path & State Graph
Time  : K-th Shortest: O(K * (V+E) log V) / State Graph: O((V*S+E) log(V*S))
Space : O(V + E) ~ O(V * S)

Explanation:
    1) K-th Shortest Path: When reaching the destination for the K-th time, it is the K-th shortest distance.
    2) State Graph Dijkstra: Expand nodes as (position, additional state)
       Example) Fuel limit, one-time toll-free coupon usage"""

import heapq
import sys

INF = sys.maxsize


# ──────────────────────────────────────────────
#1. K-th shortest path
# ──────────────────────────────────────────────

def kth_shortest_path(
    graph: list[list[tuple[int, int]]],
    src: int,
    dst: int,
    k: int,
) -> int:
    """Returns the K-th shortest path distance from src → dst.
    If unreachable or there are not K paths, returns INF.

    Principle:
      - Allow each node to be visited up to K times (visit_cnt[v] array)
      - When popping from the heap, increase visit_cnt[v]; skip if it exceeds K
      - The moment dst is popped for the K-th time, that distance is the K-th shortest distance

    Time : O(K * (V + E) log(KV))
    Space: O(K * V)"""
    n = len(graph)
    visit_cnt = [0] * n
    heap: list[tuple[int, int]] = [(0, src)]

    while heap:
        d, u = heapq.heappop(heap)
        visit_cnt[u] += 1

        if visit_cnt[u] > k:  #Exceed K times → skip
            continue
        if u == dst and visit_cnt[u] == k:
            return d

        for v, w in graph[u]:
            if visit_cnt[v] < k:
                heapq.heappush(heap, (d + w, v))

    return INF


# ──────────────────────────────────────────────
#2. State graph Dijkstra — one toll-free coupon
# ──────────────────────────────────────────────

def dijkstra_with_coupon(
    graph: list[list[tuple[int, int]]],
    src: int,
    dst: int,
) -> int:
    """Minimum cost from src→dst when you can use one toll-free coupon.

    State: (current node, whether coupon has been used)
    - Coupon unused: state=0
    - Coupon used: state=1

    dist[v][s]: minimum cost at node v with coupon state s

    Time : O((2*(V+E)) log(2V))
    Space: O(V)"""
    n = len(graph)
    # dist[node][coupon_used]
    dist = [[INF, INF] for _ in range(n)]
    dist[src][0] = 0
    # (cost, node, coupon_used)
    heap: list[tuple[int, int, int]] = [(0, src, 0)]

    while heap:
        cost, u, used = heapq.heappop(heap)
        if cost > dist[u][used]:
            continue

        for v, w in graph[u]:
            #Move without using coupon
            nc = cost + w
            if nc < dist[v][used]:
                dist[v][used] = nc
                heapq.heappush(heap, (nc, v, used))

            #Use coupon (only if not used yet)
            if used == 0:
                nc_free = cost  #This edge is free
                if nc_free < dist[v][1]:
                    dist[v][1] = nc_free
                    heapq.heappush(heap, (nc_free, v, 1))

    return min(dist[dst])


# ──────────────────────────────────────────────
#3. State graph Dijkstra — fuel limit
# ──────────────────────────────────────────────

def dijkstra_with_fuel(
    graph: list[list[tuple[int, int]]],
    fuel_graph: list[list[tuple[int, int]]],
    src: int,
    dst: int,
    max_fuel: int,
) -> int:
    """Shortest distance when fuel is consumed with each move and can only be recharged at specific nodes. 
    Simplified: fuel consumption = edge weight, fuel_graph is the set of charging nodes.

    Actual state: (node, current fuel)
    In this example, we only check the possibility of reaching with simple fuel limits.

    Time: O((V * F + E * F) log(V * F)) where F = max_fuel
    Space: O(V * F)"""
    n = len(graph)
    #dist[node][fuel]: minimum distance to this state
    dist = [[INF] * (max_fuel + 1) for _ in range(n)]
    dist[src][max_fuel] = 0
    heap: list[tuple[int, int, int]] = [(0, src, max_fuel)]

    refuel_nodes = {v for edges in fuel_graph for v, _ in edges}

    while heap:
        cost, u, fuel = heapq.heappop(heap)
        if cost > dist[u][fuel]:
            continue
        if u == dst:
            return cost

        for v, w in graph[u]:
            if fuel < w:
                continue  #Not enough fuel
            new_fuel = fuel - w
            #Refuel when reaching a charging node
            if v in refuel_nodes:
                new_fuel = max_fuel
            nc = cost + w
            if nc < dist[v][new_fuel]:
                dist[v][new_fuel] = nc
                heapq.heappush(heap, (nc, v, new_fuel))

    return INF


def main() -> None:
    #── K-th shortest path ──
    print("=" * 55)
    print("1. K-th shortest path (directed graph)")
    print("=" * 55)

    edges = [
        (0, 1, 1), (0, 2, 4),
        (1, 2, 2), (1, 3, 5),
        (2, 3, 1),
    ]
    n = 4
    g: list[list[tuple[int, int]]] = [[] for _ in range(n)]
    for u, v, w in edges:
        g[u].append((v, w))

    for k in range(1, 5):
        d = kth_shortest_path(g, 0, 3, k)
        label = str(d) if d < INF else "INF (none)"
        print(f"{k}-th shortest path (0→3): {label}")

    #── Toll-free coupon ──
    print()
    print("=" * 55)
    print("2. One toll-free coupon")
    print("=" * 55)

    # 0 --10-- 1 --10-- 3
    # |                 |
    # 1                 1
    # |                 |
    #2 ---1--- (highway, toll 100)--- 3
    edges2 = [
        (0, 1, 10), (1, 3, 10),
        (0, 2, 1),  (2, 3, 100),
    ]
    g2: list[list[tuple[int, int]]] = [[] for _ in range(4)]
    for u, v, w in edges2:
        g2[u].append((v, w))

    normal = INF  #Normal Dijkstra result
    heap_tmp: list[tuple[int, int]] = [(0, 0)]
    dist_tmp = [INF] * 4
    dist_tmp[0] = 0
    import heapq as hq
    while heap_tmp:
        d, u = hq.heappop(heap_tmp)
        if d > dist_tmp[u]:
            continue
        for v, w in g2[u]:
            if dist_tmp[u] + w < dist_tmp[v]:
                dist_tmp[v] = dist_tmp[u] + w
                hq.heappush(heap_tmp, (dist_tmp[v], v))
    normal = dist_tmp[3]

    coupon_result = dijkstra_with_coupon(g2, 0, 3)
    print(f"Without coupon 0→3: {normal}")
    print(f"With 1 coupon 0→3: {coupon_result}")
    print(f"Cost saving: {normal - coupon_result}")

    print()
    print("=" * 55)
    print("Summary of application patterns")
    print("=" * 55)
    print("K-th shortest: allow visit_cnt[v] <= K")
    print("Add state: extend dist with (node, state) as compound key")
    print("Coupon/fuel: add 1 dimension to state → O(V*S log(V*S))")


if __name__ == "__main__":
    main()

Assignment

Solve it directly on Baekjoon Online Judge first, then check the reference solution below.


Problem List

# Baekjoon Number Title Difficulty Key Technique
1 1753 Shortest Path Gold IV Standard Dijkstra
2 1916 Finding Minimum Cost Gold V Single Destination Shortest Path
3 1238 Party Gold III Reverse Graph Dijkstra

Problem 1: Baekjoon 1753 — Shortest Path (Gold IV)

Problem Summary

Find the shortest distance from a given starting point to all vertices in a directed graph.

Input

V E          (Number of vertices, number of edges)
K            (Starting vertex)
u v w        (Edge: u→v, weight w)
...

Constraints

  • 1 ≤ V ≤ 20,000
  • 1 ≤ E ≤ 300,000
  • Weight 1 ≤ w ≤ 10

Solution Strategy

  • Apply standard heapq Dijkstra
  • Print "INF" for unreachable vertices

Solution File

boj_1753.py


Problem 2: Baekjoon 1916 — Finding Minimum Cost (Gold V)

Problem Summary

Find the minimum cost bus from city A to city B in a directed graph.

Input

n            (number of cities)
m            (number of buses)
a b c        (cost from a to b c)
...
start end    (departure, arrival)

Constraints

  • 1 ≤ n ≤ 1,000
  • 1 ≤ m ≤ 100,000
  • Cost 0 ≤ c ≤ 100,000

Solution Strategy

  • Run Dijkstra and print dist[end]

Solution File

boj_1916.py


Problem 3: Baekjoon 1238 — Party (Gold III)

Problem Summary

N students go to the party in village number X and return. Travel distance = Home → X + X → Home. Find the maximum value of this.

Input

N M X        (number of students, number of roads, party village)
a b t        (a→b, time t)
...

Constraints

  • 1 ≤ N ≤ 1,000
  • 1 ≤ M ≤ 10,000
  • Weight 1 ≤ t ≤ 100

Solution Strategy

  1. Dijkstra from X in the forward graph → shortest distance X→i
  2. Dijkstra from X in the reverse graph → shortest distance i→X (X→i in the reversed graph)
  3. max(go[i] + back[i]) output

Reverse graph technique: reverse all edges and run one additional Dijkstra.

Solution Files

boj_1238.py


Pre-submission Checklist

  • Use sys.stdin for input speed
  • dist = [INF] * (V + 1) (note 1-indexed)
  • lazy deletion: if d > dist[u]: continue
  • If unreachable → "INF" or the format required by the problem statement
  • Construct the reverse graph separately by reversing all edge directions
Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

boj_1238.pyPython
"""Baekjoon 1238 — Party
https://www.acmicpc.net/problem/1238

Difficulty: Gold III
Time: O((N + M) log N) — Dijkstra twice
Space: O(N + M)

Solution:
    Round-trip distance for student i = dist_to_X[i] + dist_from_X[i]

    dist_from_X: Run Dijkstra once from X on the forward graph
    dist_to_X  : Run Dijkstra once from X on the reversed graph
                 (Distance from X to i in the reversed graph = Distance from i to X in the forward graph)

    Technique for reversed graph: flip edge (a, b, t) → (b, a, t)"""

import heapq
import sys

input = sys.stdin.readline
INF = sys.maxsize


def dijkstra(
    graph: list[list[tuple[int, int]]],
    src: int,
    n: int,
) -> list[int]:
    """Returns the shortest distance array from src in the graph.

    Time: O((N + M) log N)
    Space: O(N + M)"""
    dist = [INF] * (n + 1)
    dist[src] = 0
    heap: list[tuple[int, int]] = [(0, src)]

    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:
            continue
        for v, w in graph[u]:
            nd = dist[u] + w
            if nd < dist[v]:
                dist[v] = nd
                heapq.heappush(heap, (nd, v))

    return dist


def solve() -> None:
    N, M, X = map(int, input().split())

    graph_fwd: list[list[tuple[int, int]]] = [[] for _ in range(N + 1)]
    graph_rev: list[list[tuple[int, int]]] = [[] for _ in range(N + 1)]

    for _ in range(M):
        a, b, t = map(int, input().split())
        graph_fwd[a].append((b, t))   #Forward direction
        graph_rev[b].append((a, t))   #Reverse direction (flip b→a)

    #X → shortest distance to all students (forward direction)
    dist_from_x = dijkstra(graph_fwd, X, N)

    #Shortest distance from all students → X = distance from X → i in the reverse graph
    dist_to_x = dijkstra(graph_rev, X, N)

    #Maximum round-trip distance
    ans = max(dist_to_x[i] + dist_from_x[i] for i in range(1, N + 1))
    print(ans)


if __name__ == "__main__":
    solve()
boj_1753.pyPython
"""Baekjoon 1753 — Shortest Path
https://www.acmicpc.net/problem/1753

Difficulty: Gold IV
Time: O((V + E) log V)
Space: O(V + E)

Solution:
    Directed graph, single source (K) Dijkstra.
    Vertices that are unreachable should print "INF"."""

import heapq
import sys

input = sys.stdin.readline
INF = sys.maxsize


def solve() -> None:
    V, E = map(int, input().split())
    K = int(input())

    graph: list[list[tuple[int, int]]] = [[] for _ in range(V + 1)]
    for _ in range(E):
        u, v, w = map(int, input().split())
        graph[u].append((v, w))

    #Dijkstra
    dist = [INF] * (V + 1)
    dist[K] = 0
    heap: list[tuple[int, int]] = [(0, K)]

    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:
            continue
        for v, w in graph[u]:
            nd = dist[u] + w
            if nd < dist[v]:
                dist[v] = nd
                heapq.heappush(heap, (nd, v))

    #Output
    result = []
    for i in range(1, V + 1):
        result.append(str(dist[i]) if dist[i] < INF else "INF")
    print("\n".join(result))


if __name__ == "__main__":
    solve()
boj_1916.pyPython
"""Baekjoon 1916 — Finding the Minimum Cost
https://www.acmicpc.net/problem/1916

Difficulty: Gold V
Time: O((V + E) log V)
Space: O(V + E)

Solution:
    Directed graph, single source-destination shortest path.
    Run Dijkstra from the starting point and output the distance to the destination."""

import heapq
import sys

input = sys.stdin.readline
INF = sys.maxsize


def solve() -> None:
    n = int(input())
    m = int(input())

    graph: list[list[tuple[int, int]]] = [[] for _ in range(n + 1)]
    for _ in range(m):
        a, b, c = map(int, input().split())
        graph[a].append((b, c))

    start, end = map(int, input().split())

    #Dijkstra
    dist = [INF] * (n + 1)
    dist[start] = 0
    heap: list[tuple[int, int]] = [(0, start)]

    while heap:
        d, u = heapq.heappop(heap)
        if d > dist[u]:
            continue
        #Early termination upon reaching the destination (optional optimization)
        if u == end:
            break
        for v, w in graph[u]:
            nd = dist[u] + w
            if nd < dist[v]:
                dist[v] = nd
                heapq.heappush(heap, (nd, v))

    print(dist[end])


if __name__ == "__main__":
    solve()

Learn this lesson with a mentor

Work through the Python Algorithms & Data Structures track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now