Lecture from: 21.11.2024 | Rui Zhangs Notes | Video: Videos ETHZ

Graph Algorithms

Over the past two weeks, we’ve delved into graph algorithms, focusing on Eulerian walks and Topological Sorting. While distinct problems, the approaches we used to develop algorithms for these problems share some key similarities.

Common Algorithmic Approach

Both algorithms employed a strategy of iterative walk construction:

  1. Iterative Construction: We built walks within the graph until a specific condition halted further progress.
  2. Stopping Condition (Eulerian Walks): In the case of Eulerian walks, we continued extending the walk until encountering a situation where the only way to proceed involved traversing an edge that was already part of the walk (edge repetition).
  3. Stopping Condition (Topological Sort): For topological sorting, we continued extending the walk until we reached a vertex that had already been visited. This implied the existence of a cycle in the directed graph. This was a simplified view and there are other conditions we looked at.

Key Findings and Generalizations

The algorithms for both problems enabled us to make important observations and generalizations:

Eulerian Walks

  • Cycle Detection: By attempting to construct an Eulerian walk, we demonstrated that if a non-repeating walk exists (i.e., an Eulerian path), then that walk must be a cycle.
  • Generalization: We broadened this observation to the general principle: if a cycle exists within the graph, then our algorithm is guaranteed to find it.

Topological Sort

  • Sink Detection: In the context of topological sorting, we showed that if our constructed walk terminates at a vertex without any outgoing edges (a sink), then that vertex can be added to the topological order.
  • Generalization: Here, we showed that sinks are a requirement for Toposort and when visiting new nodes, we can discover these sinks.

Summary and Connection

In both cases, our algorithms helped us identify specific structural properties of the graphs (cycles in Eulerian walks, sinks in topological sort). The presence or absence of these properties then informs us about the existence or non-existence of solutions to the respective problems. If we can find cycles, we can construct an Eulerian path. If we can find sinks, then we can build a valid topological ordering.

Road Networks and Shortest Paths

Imagine we have data representing a road network. Our goal is to find a path with the fewest intersections.

Representing this road network as a graph, we define:

  • Nodes: Intersections
  • Edges: Road segments between intersections

We seek the shortest walk (or path) between two given intersections. Edges may be directed, representing one-way streets. Note that we can equivalently search for the shortest path instead of the shortest walk. Any walk containing duplicate vertices implies the traversal of a cycle. Such a cycle can be eliminated to create a shorter path, as illustrated below:

This problem of finding the shortest path appears in various contexts. For example, the Edit Distance dynamic programming problem (07 Dynamic Programming, Jump Game, Longest Common Subsequence, Edit Distance) can be formulated as a graph problem.

Here, the larger, elongated nodes (those with more than three edges) represent matching subsequences. They are visually distinct because there’s no “cost” associated with matching characters. This allows us to “skip” edit operations (insertions, deletions, substitutions) when characters are already equal, effectively traversing these elongated nodes at zero cost.

Definitions

Let denote the shortest path length (number of edges) between vertices and . For simplicity, we assume each edge has a length of 1.

Breadth-First Search (BFS)

Given a graph , a common task is to find the shortest paths from a source vertex to all other reachable vertices . A key insight is that computing for all reachable is computationally similar to finding the shortest path to a single vertex. Thus, we focus on calculating for all reachable from . Breadth-First Search (BFS) provides an efficient algorithm for this purpose.

Instead of directly asking for the distance between and , we consider the set of vertices reachable from at a distance . In other words, we seek the set of vertices for which .

Consider the following graph, starting at node A:

BFS explores the graph in layers, radiating outwards from the source vertex . It systematically visits all neighbors of first, then their neighbors, and so on. This layered exploration guarantees that we find the shortest paths (in terms of the number of edges) to all reachable vertices. We can disregard cycles, as any cycle would imply a longer path that we’ve already discovered a shortcut for.

The following illustration shows the distances of each vertex from A:

This structure is called a shortest path tree. It represents the shortest paths from the source vertex to all other reachable vertices. A level is defined as the set . In other words, contains all vertices at a distance from the source .

Determining Level

An edge from a vertex in to a vertex in implies that . This is because traversing an edge increases the distance from the source by at most 1. In an undirected graph, this relationship becomes because an edge can also lead to a vertex in the previous level.

Assume we have already computed the sets . How do we compute ?

A vertex belongs to (meaning the shortest distance from to is ) if and only if both of the following conditions hold:

  1. Not in Previous Levels (): This condition is crucial for ensuring that we are indeed finding the shortest distance to . If were already present in a level closer to (say where ), then would be , not . We want to avoid including vertices in that have shorter paths.

  2. Successor of a Vertex in ( is a neighbor of some ): This condition establishes that is reachable from in exactly steps. Since every vertex in is at distance from , and is a neighbor of a vertex in , the distance from to must be .

These two conditions together precisely define the set of vertices that are at distance from the source vertex . Condition 1 excludes vertices that are closer to , and condition 2 ensures that the vertices are reachable in exactly steps.

Algorithm Sketch

The algorithm to compute the shortest path distances from a source vertex s is as follows:

def shortest_path_distances(G, s):
    """
    Computes shortest path distances from a source vertex using BFS.
 
    Args:
        G: The graph represented as an adjacency list (dictionary).
        s: The source vertex.
 
    Returns:
        A list of sets S_0, S_1, ..., S_{n-1}, where S_k = {v | d(s, v) = k}.
    """
 
    n = len(G)  # Number of vertices
    S = [set() for _ in range(n)]  # Initialize list of sets
    S[0] = {s}
    marked = {v: False for v in G}  # Keep track of visited vertices
    marked[s] = True
 
    for k in range(1, n):
        for u in S[k-1]:
            for v in G[u]:  # Iterate through neighbors of u
                if not marked[v]:
                    S[k].add(v)
                    marked[v] = True  # Mark v as visited
    return S
 
  1. Marking Nodes: Once the distance to a node is known, we mark it to avoid redundant processing. This allows constant-time checks for whether a vertex has already been assigned a distance. We use a shared data structure (e.g., a boolean array or a hash set) to store this marking information. The time complexity for this step is , where is the number of vertices.

  2. Iterating through : The outer loop iterates through the sets to process vertices at distance . In the worst case, .

  3. Building : For each vertex , we iterate through its neighbors and add any unmarked neighbors to the set . The runtime of this step depends on how efficiently we can access the neighbors of . With an adjacency list representation, the total work across all iterations is proportional to the number of edges . The time complexity for this step is .

Using an adjacency list representation for the graph, the runtime of the algorithm is where is the number of vertices and is the number of edges. This is because each vertex and each edge are processed at most once. With an adjacency matrix, the runtime would be because we would need to iterate through all n possible neighbors for each vertex.

Using a Queue for Efficient BFS Implementation

While the algorithm described previously correctly computes shortest path distances, its direct implementation can be inefficient. Specifically, repeatedly iterating through the sets to find unvisited neighbors can lead to redundant checks. A queue data structure provides a more efficient way to organize the exploration process.

A queue operates on the First-In, First-Out (FIFO) principle. Elements are added to the rear of the queue (enqueued) and removed from the front (dequeued).

Why Use a Queue?

  1. Systematic Exploration: A queue ensures that we explore the graph level by level. Vertices at distance are enqueued only after all vertices at distance have been processed. This maintains the BFS’s layered exploration property, crucial for finding shortest paths.

  2. Avoid Redundant Checks: Once a vertex is enqueued, we know its distance from the source. When we dequeue it, we process its neighbors. We enqueue each unvisited neighbor only once. This eliminates the need for the check in the previous algorithm, saving significant computation. The marking of visited vertices still prevents processing a vertex multiple times.

  3. Efficient Construction: The queue implicitly maintains the sets Sₖ. The elements currently in the queue represent the “frontier” of exploration – vertices at the current distance being expanded.

Modified Algorithm with a Queue

from collections import deque
 
def bfs_shortest_paths(G, s):
  """Computes shortest path distances using BFS with a queue."""
  dist = {v: float('inf') for v in G}
  dist[s] = 0
  q = deque([s])  # Initialize queue with the source vertex
  parent = {} #dictionary to maintain shortest path tree
  
  while q:
    u = q.popleft()
    for v in G[u]:
        if dist[v] == float('inf'):
          dist[v] = dist[u] + 1
          parent[v] = u # u is parent of v
          q.append(v)
  return dist, parent # return distances and the shortest path tree

Runtime Analysis with a Queue

The queue-based BFS algorithm maintains the same time complexity as the previous version but is more efficient in practice due to:

  • Constant-time Enqueue/Dequeue: Queue operations take constant time, ensuring efficient addition and removal of vertices.
  • No Set Operations: Eliminating the need for set union operations () significantly reduces overhead, especially for dense graphs. Only constant time checks for already visited vertices remain necessary.

The use of a queue drastically improves the practical efficiency of BFS, making it the preferred method for computing shortest paths in unweighted graphs. The parent dictionary returned alongside the dist dictionary can be used to efficiently reconstruct the actual shortest path from the source s to any reachable vertex.

BFS: Enter and Leave Times

The concept of enter and leave times provides valuable insights into the execution and properties of the Breadth-First Search (BFS) algorithm. These timestamps, associated with each vertex, capture the order in which vertices are first encountered (entered) and finished processing (left) during the BFS traversal.

Definitions and Notations

  • enter[v]: The time (or step) at which vertex is first encountered and added to the queue.
  • leave[v]: The time at which vertex is dequeued and its neighbors are processed.
  • T: A global counter representing the current time step. It is initialized to 1 and incremented each time a vertex is entered or left.

We can now visualize the intervals:

Observations

Part of the notes, wasn’t looked at in the lecture…

  1. enter[v] < leave[v]: A vertex is always entered before it is left. This is a fundamental property of the BFS traversal since each vertex is visited and processed in a single operation once it is dequeued.

  2. enter-order = leave-order (within levels): Within a given level (vertices at distance k from the source), the enter times and leave times respect the same relative ordering. If a vertex u is entered before vertex v in the same level, it will also be left before v. This is due to the FIFO nature of the queue and the layered exploration strategy of BFS. However, this is not true across different levels, where vertices with larger distances will have both enter and leave times larger than close-by vertices.

  3. Epochs and : The interval defines an epoch during the BFS execution. is the time when the first vertex at distance is dequeued (and left). signifies the end of level exploration, and the beginning of level or the completion of the BFS traversal if no vertices at level exist. , the set of vertices left in the interval , is equivalent to the set .

Justification and Proof

Part of the notes, wasn’t looked at in the lecture…

The core idea is to prove that the set of vertices (at distance k from the source) is equivalent to the set (vertices with leave times in the interval ). Formally:

where .

We want to show that the set of vertices at distance from the source () is identical to the set of vertices whose leave times fall within the interval (). Here, is the time the first vertex at distance is dequeued. This means that the vertices in are precisely those dequeued between the start of processing level and the start of processing level .

1. Key Idea: BFS explores the graph layer by layer. The queue always contains vertices from the current level and the next. Once a level is completely processed, all vertices at that level have left the queue.

2. Base Case ():

The source vertex is at distance 0 (). It’s the first vertex enqueued and dequeued. Thus, its leave time is , and . Therefore, .

3. Inductive Hypothesis:

Assume that for some , the statement holds true. This means that all vertices at distance are precisely those with leave times in .

4. Inductive Step:

Let’s consider a vertex at distance ().

  • enters the queue: is added to the queue only when its parent, , is dequeued. Since is at distance (), by the inductive hypothesis, its leave time is in the interval .
  • ’s leave time: Because BFS explores level by level, all vertices at distance must leave the queue before any vertex at distance is dequeued. Therefore, leave[v] must be greater than or equal to .
  • Upper bound on leave[v]: Since marks the start of processing level , all vertices at level must leave the queue before this time. Therefore, leave[v] must be strictly less than .
  • Conclusion: Therefore, if , then , implying .

Now, let’s consider a vertex such that (). This means is dequeued during the processing of level . Since BFS processes vertices level by level, the distance of from the source must be . Therefore, .

5. Conclusion:

We have shown that if , then , and vice-versa. Therefore, for all , completing the proof by induction.

Cheapest Walks in Weighted Graphs

In weighted graphs, each edge is assigned a cost or weight. The cost of a walk is the sum of the weights of its constituent edges. The goal is often to find the cheapest or shortest path between two vertices, which corresponds to the walk with the minimum total cost.

Intro to Shortest Paths Problem (MIT)

I suggest you watch this good introduction to get intuition behind what’s going on…

Definitions and Notations

  • Weighted Graph: , where is the set of vertices, is the set of edges, and is a cost function assigning a real-valued weight to each edge.
  • Cost of a Walk: Given a walk , its cost is defined as:
  • Cheapest Path (Shortest Path): The path between vertices and with the minimum cost is denoted as the cheapest path or shortest path.

  • Distance: The cost of the cheapest path between vertices and is denoted as the distance . Formally:

Negative Costs and Cycles

Allowing negative edge costs introduces the possibility of negative cycles – cycles whose total weight is negative. If a negative cycle is reachable from the source vertex, the shortest path problem becomes ill-defined, as traversing the cycle repeatedly would lead to arbitrarily low path costs (). Therefore, algorithms for finding shortest paths often assume the absence of negative cycles or explicitly detect them. More on this later…

Notation and Conventions

  • Edge Notation: An edge from vertex to vertex is denoted as or as .
  • Path Notation: A path is a sequence of vertices such that for all .
  • Walk Notation: Similar to a path but may contain repeated vertices or edges.

Structure of Cheapest Paths

A key property of cheapest paths in weighted graphs (without negative cycles) is the optimal substructure property. This means that any subpath of a cheapest path is itself a cheapest path between its endpoints. This property is crucial for the design of dynamic programming algorithms like Dijkstra’s algorithm and the Bellman-Ford algorithm.

Example

Consider this graph:

  • If , the cheapest path from A to C has a cost of 6 (directly from A to C).
  • If , the cheapest path from A to C has a cost of (A to B to C).
  • If , the cycle A-B-C-A becomes negative, making the shortest path to C from A undefined as it can be arbitrarily small.

This example illustrates how negative edge weights can lead to undefined shortest paths in the presence of negative cycles.

Cheapest Walks in Weighted Graphs

When dealing with weighted graphs, where edges have associated costs (weights), the goal often shifts from finding the shortest path (in terms of number of edges) to finding the cheapest path (minimum total weight). The presence of negative edge weights introduces complexities not present in unweighted graphs or graphs with non-negative weights.

Negative Weights and the Absence of Negative Cycles

Allowing negative edge weights opens the possibility of negative cycles. A negative cycle is a cycle where the sum of the weights of its edges is negative. The existence of a negative cycle fundamentally changes the problem:

  • Infinitely Cheap Walks: If a negative cycle exists, it’s possible to construct arbitrarily cheap walks by repeatedly traversing the cycle. There is no minimum cost path or walk.
  • No Shortest Path: The concept of a “shortest” or “cheapest” path becomes meaningless because any path can be made cheaper by adding more traversals of a negative cycle.

Therefore, to ensure the existence of a cheapest path, we must assume that no negative cycles exist in the graph. This assumption guarantees that any walk can be shortened to a path without increasing its cost. Any walk containing a cycle can have that cycle removed, leading to a path with the same or lower cost.

Triangle Inequality

The triangle inequality is a fundamental property of shortest paths (and cheapest walks in the absence of negative cycles) in a weighted graph.

Visually:

For any three vertices , , and , the following inequality must hold:

where represents the cost of the cheapest path (or walk) between vertices and .

Intuitively, the triangle inequality asserts that the direct path from to cannot be more expensive than going through an intermediary vertex . If it were, then the path through would not be optimal, violating the definition of . This property is crucial for many shortest-path algorithms.

Structure of Cheapest Paths: Subpath Optimality

A crucial property of cheapest paths in graphs without negative cycles is the principle of subpath optimality. This states that if a path from vertex to vertex is a cheapest path (), then any subpath of is also a cheapest path between its endpoints.

More formally: Let be a cheapest path from to . Then, for any , the subpath is a cheapest path from to .

Proof:

We’ll prove this by contradiction. Suppose that the subpath is not a cheapest path from to . This means there exists a shorter path from to with a lower cost. We can then construct a new path from to as follows:

  1. Take the portion of from to .
  2. Append the path .
  3. Append the portion of from to .

This new path has a lower cost than because we replaced a more expensive subpath with a cheaper one (). This contradicts our initial assumption that was a cheapest path from to . Therefore, the subpath must also be a cheapest path from to .

The Importance of No Negative Cycles: This property crucially relies on the absence of negative cycles. If negative cycles existed, we could create cheaper paths by introducing cycles, invalidating the subpath optimality principle. The proof relies on the fact that replacing a subpath with a shorter one directly leads to a shorter overall path. This wouldn’t hold if negative cycles allowed us to make the entire path arbitrarily cheap by adding negative-cost cycles within it.

Cheapest Paths: Recurrence Relation and Dynamic Programming

We can formulate the problem of finding cheapest paths using a recurrence relation. Let represent the cost of the cheapest path from vertex to vertex . We can express recursively as:

Where:

  • is the set of edges in the graph.
  • is the cost (weight) of the edge from to .
  • The minimum is taken over all outgoing edges from . If there are no outgoing edges from to any other vertices, then no paths exist to any other vertices from , and the cost is .

This recurrence says that the cheapest path from to is either 0 if and are the same vertex, or the minimum cost among all paths that go from to some neighbor and then continue to from there using the cheapest path from to .

Dynamic Programming Approach

This recurrence relation naturally lends itself to a dynamic programming (DP) solution. We can compute the values of iteratively using a table (or matrix) to store the results. A naive recursive implementation of the recurrence relation, while conceptually straightforward, is highly inefficient. It suffers from redundant calculations and, in the presence of cycles, can lead to infinite recursion. Therefore, a DP approach is essential. We avoid memoization (top-down DP) in favor of a bottom-up DP strategy because of the risk of infinite recursion due to cycles. A smart calculation order is required to guarantee that we compute all the necessary distances before they’re needed.

Calculation Order

The choice of calculation order is crucial to ensure that when computing the shortest distance from to , all necessary values (shortest distances from neighbors of to ) have already been computed. This avoids the pitfalls of recursion and ensures that we get the optimal solution. Several approaches can efficiently determine this order:

  • Iterative Approach (Bellman-Ford): This approach iteratively relaxes all edges in the graph. In each iteration, the algorithm processes all edges and updates the distances if a shorter path is found. This process repeats for iterations, where is the number of vertices. After this the algorithm checks for negative cycles. If no negative cycles exist, the algorithm will have correctly computed the shortest paths to all vertices reachable from the source. If negative cycles exist, this will be detected in this final checking step. The key is that each iteration ensures all distances from a vertex to each of its reachable vertices are calculated before moving to the next iteration.

  • Topological Sort (for Directed Acyclic Graphs): For directed acyclic graphs (DAGs), topological sorting provides a highly efficient calculation order. A topological sort orders vertices such that for every directed edge , vertex precedes vertex in the ordering. Processing vertices in this order guarantees that when we compute , the distances for all neighbors of have already been computed. This is because the topological order ensures we process all predecessors before their successors. There are no cycles to cause computational problems or issues with dependencies.

The choice between Bellman-Ford and the topological sort approach depends on the graph structure. Bellman-Ford is more general and handles graphs with cycles (provided no negative cycles exist), while topological sorting is more efficient for DAGs (Directed Acyclic Graphs). The use of either of these methods replaces the inefficient top-down recursive solution based directly on the recurrence relation with an efficient iterative approach that avoids recursive loops by imposing a well-defined computation order.

DAG shortest path and Dijkstra's Algorithm (MIT)

Once again, I suggest watching this video to get a better understanding…

Acyclical Graphs and Topological Sorting

In directed acyclic graphs (DAGs), the absence of cycles simplifies the computation of cheapest paths significantly. Topological sorting provides an optimal ordering for the dynamic programming approach.

Topological Sorting: A topological sort of a DAG is a linear ordering of all its vertices such that for every directed edge from vertex to vertex , vertex comes before vertex in the ordering.

Algorithm:

  1. Topological Sort: Obtain a topological ordering of the vertices (various algorithms exist for this, often using depth-first search).
  2. Initialization: Initialize the distance array d[] as follows: d[source] = 0, d[v] = ∞ for all other vertices v.
  3. Iteration: Iterate through the vertices in topological order. For each vertex u, consider its outgoing edges . If d[u] + c(u, v) < d[v], update d[v] to d[u] + c(u, v).

Runtime: Topological sorting takes time. The subsequent iterative step also takes time. Thus, the overall algorithm has a linear time complexity. This is a significant improvement over algorithms that work on general graphs (like Bellman-Ford), which can have a runtime of .

Cheapest Paths in Graphs with Non-Negative Edge Costs

Finding the cheapest paths in graphs with non-negative edge costs is a fundamental graph problem with numerous applications. The absence of negative edge weights simplifies the problem significantly, allowing for efficient algorithms like Dijkstra’s algorithm. However, understanding the underlying principles is important for appreciating the algorithm’s efficiency and correctness.

Assumptions and Definitions

  1. Non-negative Edge Costs: We assume that the cost (weight) of every edge in the graph is non-negative. This is crucial for the correctness and efficiency of the algorithms discussed here.

  2. Weighted Graph: The graph is a weighted directed graph, where is the set of vertices and is the set of directed edges. Each edge has a non-negative weight .

  3. Cheapest Path: For any pair of vertices , we denote the cost of the cheapest path from to as . If no path exists from to , then .

Recurrence Relation

The cost of the cheapest path from vertex to vertex , denoted as , can be expressed recursively:

This recurrence relation states that the cheapest path from to :

  • Is 0 if and are the same vertex.
  • Otherwise, it’s the minimum cost among all paths that go from to a neighbor (via an edge ), and then continue from to along a cheapest path. The cost of such a path is the edge cost plus the cost of the cheapest path from to , which is .

Important Note on Calculation Order and Implicit Sorting

To avoid infinite recursion and ensure efficient computation, we need to carefully consider the order in which we compute values. Algorithms like Dijkstra’s implicitly sort vertices based on their distances from the source. We can think of this as iteratively considering vertices in increasing order of distance from the source.

Let’s assume we have already computed the shortest distances to vertices from the source and that we have for . When computing for a given vertex , we compute:

Why the condition ? The condition ensures that when calculating , we only consider paths that go through vertices whose shortest distances from the source have already been computed. This is crucial because the recurrence relation relies on having already calculated the shortest distances to the predecessors before we can compute the shortest distance to the current vertex.

If we didn’t have this condition, the algorithm could potentially consider paths that are not yet optimal, leading to incorrect results. The implicit sorting ensures that this condition is always met, leading to a correct and efficient calculation of shortest distances.

Proof of Correctness (for Non-Negative Edge Costs)

We want to prove that for any vertex , the cheapest path from the source to is correctly computed by the recurrence: . We’ll assume vertices are considered in non-decreasing order of their distances from the source.

  1. Subpath Optimality: As before, this proof relies on the subpath optimality property, which holds for graphs with non-negative edge costs.

  2. Contradiction Setup: Assume, for the sake of contradiction, that there exists a vertex for which the recurrence calculates an incorrect shortest path cost. This means there is a cheaper path to than the one found by the recurrence.

  3. The Contradiction: Let’s assume that the last edge of the actual cheapest path to is from some vertex to with . This means we are considering a vertex () that comes after in the order of non-decreasing distance from the source. Since the costs are non-negative we have:

(because and we process vertices in order of increasing distance from )

If this contradicts the assumption of the cheapest path using as a predecessor since there would exist a better path using the shortest path from to calculated earlier. This implies . If can be reached via a shorter path at all, this shortest path cannot traverse a vertex processed later. If a shorter path uses only vertices processed earlier, this path would be found by the recurrence relation.

Therefore, our initial assumption of an incorrect shortest path cost calculated by the recurrence must be false. This concludes the proof by contradiction.

A Useful Recurrence Relation and Algorithm for Cheapest Paths

The recurrence relation for computing cheapest paths, , initially seems circular: it requires knowing the shortest distances to compute the shortest distance, creating a dependency issue. However, this circularity can be resolved by carefully orchestrating the order of computation. The key is to compute the shortest distances and their associated ordering simultaneously.

Core Idea: The algorithm iteratively builds a set of vertices for which we’ve definitively determined the cheapest paths from the source vertex . Crucially, vertices are added to in non-decreasing order of their shortest path distances from . This ordering is what breaks the circular dependency in the recurrence relation.

Recurrence Relation and Notation

Let denote the shortest distance from the source to vertex considering only paths that use vertices from the set . This allows us to define a non-circular recurrence:

This recurrence states:

  • The distance from to is 0.
  • If is already in , its shortest distance is already known.
  • If is not in , we consider all incoming edges from vertices in and choose the path with the minimum cost. This path is composed of the shortest path from to some vertex in , followed by the edge from to .
  • If there’s no path from to , the distance is infinity.

The Algorithm’s Strategy: The algorithm repeatedly selects the vertex with the minimum value from among the vertices not yet in . The key is that, because the algorithm expands this set in this manner, by the time it examines a vertex, the shortest distances to all its predecessors will be already computed, making the recurrence non-circular and allowing the algorithm to correctly compute . Then is added to , and the distances to its neighbors are updated based on the recurrence, iteratively solving the problem efficiently.

Proof of Correctness

The algorithm maintains a set of vertices whose shortest distances from the source have been definitively determined. The algorithm iteratively adds vertices to by selecting the vertex with the smallest tentative distance , where:

Here, is the known shortest distance from to (since ), and is the cost of the direct edge from to .

Claim: When the algorithm selects , its tentative distance is equal to the true shortest path distance .

Proof by Contradiction:

  1. Assumption: Assume, for contradiction, that . This implies that there is a cheaper path from to than the one found by .

  2. Alternative Path: Let be such a cheaper path from to . Since , this path must leave the set at some point. Let be the last vertex in along the path , and let be the first vertex outside along . (Note that might be itself).

  3. Cost Analysis: The cost of the path can be expressed as:

    where:

    • is the known shortest path cost from to (since ).
    • is the cost of the edge .
    • is the cost of the subpath from to .
  4. Non-negative Edge Costs: Since all edge costs are non-negative, . Therefore:

  5. The Contradiction: We know that is the minimal path going from any node in S to , therefore . Because of the non-negative edge costs, we know that the cost of going from must at least be therefore . Since we chose such that is minimal we have . This contradicts our assumption that is a cheaper path than the one implied by .

Therefore, our initial assumption must be false, and we conclude that when is selected by the algorithm.

Note for readers: For a less confusing explanation, watch this video…

Dijkstra’s Algorithm

Dijkstra’s algorithm is a greedy algorithm that efficiently computes the shortest paths from a single source vertex to all other vertices in a weighted graph with non-negative edge costs. Its efficiency stems from a clever strategy of iteratively selecting vertices in increasing order of their shortest distance estimates from the source.

Algorithm Explanation

  1. Initialization:

    • Set the distance to the source vertex to 0 ().
    • Set the distance to all other vertices to infinity ( for all ).
    • Create a set initially containing only the source vertex (). tracks vertices whose shortest distances from the source have been definitively determined.
    • Create a priority queue containing all vertices, prioritized by their tentative distances.
  2. Iteration: While the priority queue is not empty:

    • Select the vertex with the smallest tentative distance from the priority queue .
    • Add to the set .
    • For each neighbor of such that :
    • Relax the edge : If , update to and update the priority queue accordingly.
  3. Termination: Once the priority queue is empty, the algorithm terminates, and represents the shortest distance from the source to each vertex .

Pseudocode

Alternatively:

def dijkstra(graph, start):
    # Initialize distances: all nodes are at "infinity" except the start node (0 distance to itself)
    distances = {node: infinity for each node in graph}
    distances[start] = 0
    
    # Create a priority queue to process nodes in the order of their distance from the start
    priority_queue = [(0, start)]  # (distance, node)
 
    while priority_queue is not empty:
        # Get the node with the smallest distance from the queue
        current_distance, current_node = pop the smallest item from priority_queue
        
        # For each neighboring node:
        for neighbor, weight in graph[current_node]:
            # Calculate the total distance to the neighbor
            distance = current_distance + weight
            
            # If this new distance is shorter than the previously known distance:
            if distance < distances[neighbor]:
                # Update the distance to this neighbor
                distances[neighbor] = distance
                
                # Add the neighbor to the queue for further exploration
                push (distance, neighbor) into priority_queue
 
    return distances
 

Runtime Analysis

The runtime of Dijkstra’s algorithm depends heavily on the implementation of the priority queue.

  • Using a binary heap: The priority queue operations (insertion, deletion, and finding the minimum) take time in a binary heap. Since each vertex is added to and removed from the priority queue at most once, and each edge is processed at most once, the overall runtime is , where is the number of vertices and is the number of edges.

  • Using a Fibonacci heap: Fibonacci heaps offer amortized time for decrease-key operations. This leads to an improved runtime of . However, Fibonacci heaps are more complex to implement.

In practice, the binary heap implementation is often preferred due to its simplicity and reasonable performance, especially for graphs that are not excessively large. The use of a priority queue is key to achieving the efficient runtime. Without it, the runtime would be significantly worse.

Continue here: 12 Cheapest Path Problem, Negative Cost, Bellman-Ford, Minimum Spanning Trees, Boruvka’s Algorithm, Prim’s Algorithm, Priority Queues