Golang
Golang

 

 

The Shortest Path problem is a fundamental challenge in computer science and graph theory, with applications ranging from navigation systems to network routing and beyond. Dijkstra’s Algorithm is one of the most well-known and efficient algorithms for solving this problem in graphs with non-negative edge weights. This article provides a comprehensive guide to implementing Dijkstra’s Algorithm in Go, complete with detailed explanations and well-documented code.

Problem Statement

Given a graph represented by vertices and weighted edges, the goal is to find the shortest path from a starting vertex to a target vertex such that the sum of the weights of the edges along the path is minimized. Dijkstra’s Algorithm efficiently solves this problem for graphs with non-negative edge weights.

Program Structure

The Go program to find the shortest path using Dijkstra’s Algorithm is structured as follows:

  • Graph Representation: The graph is represented using an adjacency list, where each vertex has a list of its neighboring vertices and the corresponding edge weights.
  • Priority Queue: A priority queue (min-heap) is implemented to efficiently select the vertex with the smallest tentative distance during each step of the algorithm.
  • Dijkstra’s Algorithm Implementation: The algorithm initializes distances, updates them based on edge weights, and uses the priority queue to determine the next vertex to process.
  • Main Function: Demonstrates the usage of the shortest path implementation with an example graph.

Go Program


package main

import (
    "container/heap"
    "fmt"
    "math"
)

// Edge represents an edge in the graph with a destination vertex and weight
type Edge struct {
    dest   int
    weight int
}

// Graph represents a graph using an adjacency list
type Graph struct {
    vertices int
    adjList  [][]Edge
}

// NewGraph initializes a new graph with a given number of vertices
func NewGraph(vertices int) *Graph {
    adj := make([][]Edge, vertices)
    for i := range adj {
        adj[i] = []Edge{}
    }
    return &Graph{
        vertices: vertices,
        adjList:  adj,
    }
}

// AddEdge adds a directed edge from src to dest with the given weight
func (g *Graph) AddEdge(src, dest, weight int) {
    if src >= g.vertices || dest >= g.vertices || src < 0 || dest < 0 {
        fmt.Println("Invalid edge!")
        return
    }
    g.adjList[src] = append(g.adjList[src], Edge{dest, weight})
}

// Item represents an item in the priority queue
type Item struct {
    vertex   int
    priority int
    index    int
}

// PriorityQueue implements heap.Interface and holds Items
type PriorityQueue []*Item

// Len returns the number of items in the priority queue
func (pq PriorityQueue) Len() int { return len(pq) }

// Less determines the order of items based on priority
func (pq PriorityQueue) Less(i, j int) bool {
    return pq[i].priority < pq[j].priority
}

// Swap swaps two items in the priority queue
func (pq PriorityQueue) Swap(i, j int) {
    pq[i], pq[j] = pq[j], pq[i]
    pq[i].index = i
    pq[j].index = j
}

// Push adds an item to the priority queue
func (pq *PriorityQueue) Push(x interface{}) {
    n := len(*pq)
    item := x.(*Item)
    item.index = n
    *pq = append(*pq, item)
}

// Pop removes and returns the item with the highest priority (lowest priority number)
func (pq *PriorityQueue) Pop() interface{} {
    old := *pq
    n := len(old)
    item := old[n-1]
    old[n-1] = nil // avoid memory leak
    item.index = -1
    *pq = old[0 : n-1]
    return item
}

// update modifies the priority of an item in the queue
func (pq *PriorityQueue) update(item *Item, priority int) {
    item.priority = priority
    heap.Fix(pq, item.index)
}

// Dijkstra computes the shortest path from source to all other vertices
func (g *Graph) Dijkstra(source int) ([]int, []int) {
    distances := make([]int, g.vertices)
    predecessors := make([]int, g.vertices)
    for i := 0; i < g.vertices; i++ { distances[i] = math.MaxInt32 predecessors[i] = -1 } distances[source] = 0 pq := make(PriorityQueue, 0) heap.Init(&pq) heap.Push(&pq, &Item{vertex: source, priority: 0}) for pq.Len() > 0 {
        current := heap.Pop(&pq).(*Item)
        u := current.vertex

        for _, edge := range g.adjList[u] {
            v := edge.dest
            weight := edge.weight
            if distances[u]+weight < distances[v] {
                distances[v] = distances[u] + weight
                predecessors[v] = u
                heap.Push(&pq, &Item{vertex: v, priority: distances[v]})
            }
        }
    }

    return distances, predecessors
}

// ShortestPath reconstructs the shortest path from source to target using predecessors
func (g *Graph) ShortestPath(source, target int) ([]int, int) {
    distances, predecessors := g.Dijkstra(source)
    path := []int{}
    at := target

    if distances[target] == math.MaxInt32 {
        return path, -1 // No path exists
    }

    for at != -1 {
        path = append([]int{at}, path...)
        at = predecessors[at]
    }

    return path, distances[target]
}

func main() {
    // Example usage:
    // Create a graph with 9 vertices (0 to 8)
    g := NewGraph(9)

    // Add edges (undirected)
    g.AddEdge(0, 1, 4)
    g.AddEdge(0, 7, 8)
    g.AddEdge(1, 2, 8)
    g.AddEdge(1, 7, 11)
    g.AddEdge(2, 3, 7)
    g.AddEdge(2, 8, 2)
    g.AddEdge(2, 5, 4)
    g.AddEdge(3, 4, 9)
    g.AddEdge(3, 5, 14)
    g.AddEdge(4, 5, 10)
    g.AddEdge(5, 6, 2)
    g.AddEdge(6, 7, 1)
    g.AddEdge(6, 8, 6)
    g.AddEdge(7, 8, 7)

    // Since the graph is undirected, add reverse edges
    for u := 0; u < g.vertices; u++ {
        for _, edge := range g.adjList[u] {
            g.AddEdge(edge.dest, u, edge.weight)
        }
    }

    source := 0
    target := 4
    path, distance := g.ShortestPath(source, target)

    if distance != -1 {
        fmt.Printf("Shortest path from %d to %d is %v with total distance %d.\n", source, target, path, distance)
    } else {
        fmt.Printf("No path exists from %d to %d.\n", source, target)
    }
}
        

Documentation

The Go program provided implements Dijkstra’s Algorithm to find the shortest path between two vertices in a graph. Below is a detailed explanation of each component of the program:

1. Edge Struct

The Edge struct represents an edge in the graph, containing the destination vertex dest and the weight of the edge.

2. Graph Struct and Methods

The Graph struct encapsulates the graph’s structure, including the number of vertices and an adjacency list adjList.

NewGraph Function

The NewGraph function initializes a new graph with a specified number of vertices and an empty adjacency list.

AddEdge Method

The AddEdge method allows adding a directed edge to the graph by specifying the source vertex src, destination vertex dest, and the edge’s weight. Since the graph is undirected, edges are added in both directions in the main function.

3. Priority Queue Implementation

Dijkstra’s Algorithm requires a priority queue to efficiently select the vertex with the smallest tentative distance. The program implements a priority queue using Go’s container/heap package.

Item Struct

The Item struct represents an element in the priority queue, containing the vertex, its priority (tentative distance), and its index in the heap.

PriorityQueue Type

The PriorityQueue type is a slice of pointers to Item structs and implements the heap.Interface, providing methods for heap operations.

Heap Methods

The following methods are implemented to satisfy the heap.Interface:

  • Len: Returns the number of items in the priority queue.
  • Less: Determines the ordering of items based on their priority.
  • Swap: Swaps two items in the priority queue.
  • Push: Adds an item to the priority queue.
  • Pop: Removes and returns the item with the highest priority (lowest priority number).

The update method adjusts the priority of an existing item and fixes the heap to maintain the priority queue properties.

4. Union-Find Data Structure

Although Union-Find is typically associated with Kruskal’s Algorithm, it is not used in Dijkstra’s Algorithm. Instead, Dijkstra relies on the priority queue to manage the exploration of vertices based on their tentative distances.

5. Dijkstra Function

The Dijkstra function implements Dijkstra’s Algorithm to compute the shortest path from a source vertex to all other vertices in the graph.

Steps:

  1. Initialization: Set all distances to infinity (math.MaxInt32), except for the source vertex, which is set to 0. Initialize a predecessors slice to reconstruct the shortest path.
  2. Priority Queue Setup: Initialize the priority queue with the source vertex.
  3. Traversal: While the priority queue is not empty, extract the vertex with the smallest tentative distance. For each adjacent vertex, calculate the new tentative distance and update it if it’s smaller than the previously recorded distance. Push the updated vertex into the priority queue.

6. ShortestPath Function

The ShortestPath function reconstructs the shortest path from the source to the target vertex using the predecessors slice obtained from the Dijkstra function.

Steps:

  1. Check if the target vertex is reachable (distance not equal to infinity).
  2. Reconstruct the path by traversing the predecessors slice from the target back to the source.
  3. Return the reconstructed path and the total distance.

7. Main Function

The main function demonstrates how to use the Dijkstra’s Algorithm implementation:

  1. Initialize a graph with a specified number of vertices.
  2. Add edges to the graph with corresponding weights.
  3. Define the source and target vertices.
  4. Call the ShortestPath function to find the shortest path and its distance.
  5. Print the results.

How the Program Works

  1. Graph Initialization: A graph is created with a predefined number of vertices using the NewGraph function.
  2. Adding Edges: Edges with specified weights are added to the graph using the AddEdge method. Since the graph is undirected, edges are added in both directions.
  3. Dijkstra’s Algorithm:
    • All vertices are initially assigned a tentative distance value of infinity, except for the source vertex, which is assigned a distance of 0.
    • A priority queue is used to select the vertex with the smallest tentative distance at each step.
    • The algorithm explores all adjacent vertices of the selected vertex, updating their tentative distances if a shorter path is found through the current vertex.
    • This process continues until all vertices have been processed, ensuring that the shortest paths from the source to all other vertices are determined.
  4. Path Reconstruction: Using the predecessors slice, the program reconstructs the shortest path from the source to the target vertex.
  5. Output: The shortest path and its total distance are printed to the console.

Example Output

Given the following graph with 9 vertices (0 to 8) and edges:

  • Edge between vertex 0 and 1 with weight 4
  • Edge between vertex 0 and 7 with weight 8
  • Edge between vertex 1 and 2 with weight 8
  • Edge between vertex 1 and 7 with weight 11
  • Edge between vertex 2 and 3 with weight 7
  • Edge between vertex 2 and 8 with weight 2
  • Edge between vertex 2 and 5 with weight 4
  • Edge between vertex 3 and 4 with weight 9
  • Edge between vertex 3 and 5 with weight 14
  • Edge between vertex 4 and 5 with weight 10
  • Edge between vertex 5 and 6 with weight 2
  • Edge between vertex 6 and 7 with weight 1
  • Edge between vertex 6 and 8 with weight 6
  • Edge between vertex 7 and 8 with weight 7

Running the program produces the following output:


Shortest path from 0 to 4 is [0 7 6 5 4] with total distance 19.
        

This output indicates that the shortest path from vertex 0 to vertex 4 is through vertices 7, 6, and 5, with a total distance of 19.

Conclusion

Dijkstra’s Algorithm is a powerful tool for finding the shortest paths in graphs with non-negative edge weights. The provided Go implementation efficiently models the graph using an adjacency list, leverages a priority queue for optimal vertex selection, and utilizes the predecessors slice for path reconstruction. By understanding and utilizing this implementation, developers can apply Dijkstra’s Algorithm to a wide range of practical problems in networking, navigation, and beyond.

Excerpt

Implement Dijkstra’s Algorithm in Go to find the shortest path in a graph. This guide includes detailed explanations and a well-documented Go program example.

 

By Aditya Bhuyan

I work as a cloud specialist. In addition to being an architect and SRE specialist, I work as a cloud engineer and developer. I have assisted my clients in converting their antiquated programmes into contemporary microservices that operate on various cloud computing platforms such as AWS, GCP, Azure, or VMware Tanzu, as well as orchestration systems such as Docker Swarm or Kubernetes. For over twenty years, I have been employed in the IT sector as a Java developer, J2EE architect, scrum master, and instructor. I write about Cloud Native and Cloud often. Bangalore, India is where my family and I call home. I maintain my physical and mental fitness by doing a lot of yoga and meditation.

Leave a Reply

Your email address will not be published. Required fields are marked *

error

Enjoy this blog? Please spread the word :)