Golang
Golang

 

 

Cycle detection in graphs is a fundamental problem in computer science with applications ranging from deadlock detection in operating systems to verifying the correctness of software dependencies. Detecting whether a graph contains a cycle helps in understanding the structure and behavior of the graph, which is crucial in various domains such as networking, data processing, and compiler design.

Problem Statement

Given a graph, determine whether it contains any cycles. The graph can be either directed or undirected. A cycle exists if there is a path that starts and ends at the same vertex without repeating any edges or vertices (except the starting and ending vertex).

Program Structure

The Go program to detect cycles in a graph is structured as follows:

  • Graph Representation: The graph is represented using an adjacency list, where each vertex has a list of its adjacent vertices.
  • Cycle Detection Algorithms: The program implements two primary algorithms for cycle detection:
    • Depth-First Search (DFS) Based Approach: Suitable for both directed and undirected graphs.
    • Union-Find (Disjoint Set) Based Approach: Primarily used for undirected graphs.
  • Cycle Detection Methods: Separate methods are provided for detecting cycles in directed and undirected graphs using the aforementioned algorithms.
  • Main Function: Demonstrates the usage of the cycle detection methods with example graphs.

Go Program


package main

import (
    "fmt"
)

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

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

// AddEdge adds an edge to the graph
// For undirected graphs, edges are added in both directions
func (g *Graph) AddEdge(src, dest int, directed bool) {
    if src >= g.vertices || dest >= g.vertices || src < 0 || dest < 0 {
        fmt.Println("Invalid edge!")
        return
    }
    g.adjList[src] = append(g.adjList[src], dest)
    if !directed {
        g.adjList[dest] = append(g.adjList[dest], src)
    }
}

// CycleDetectedDFS detects a cycle in a directed graph using DFS
func (g *Graph) CycleDetectedDFS() bool {
    visited := make([]bool, g.vertices)
    recStack := make([]bool, g.vertices)

    for i := 0; i < g.vertices; i++ {
        if !visited[i] {
            if g.dfsCycle(i, visited, recStack) {
                return true
            }
        }
    }
    return false
}

// dfsCycle is a helper function for CycleDetectedDFS
func (g *Graph) dfsCycle(v int, visited, recStack []bool) bool {
    visited[v] = true
    recStack[v] = true

    for _, neighbor := range g.adjList[v] {
        if !visited[neighbor] {
            if g.dfsCycle(neighbor, visited, recStack) {
                return true
            }
        } else if recStack[neighbor] {
            return true
        }
    }

    recStack[v] = false
    return false
}

// CycleDetectedDFSUndirected detects a cycle in an undirected graph using DFS
func (g *Graph) CycleDetectedDFSUndirected() bool {
    visited := make([]bool, g.vertices)

    for i := 0; i < g.vertices; i++ {
        if !visited[i] {
            if g.dfsCycleUndirected(i, -1, visited) {
                return true
            }
        }
    }
    return false
}

// dfsCycleUndirected is a helper function for CycleDetectedDFSUndirected
func (g *Graph) dfsCycleUndirected(v, parent int, visited []bool) bool {
    visited[v] = true

    for _, neighbor := range g.adjList[v] {
        if !visited[neighbor] {
            if g.dfsCycleUndirected(neighbor, v, visited) {
                return true
            }
        } else if neighbor != parent {
            return true
        }
    }
    return false
}

// CycleDetectedUnionFind detects a cycle in an undirected graph using Union-Find
func (g *Graph) CycleDetectedUnionFind() bool {
    uf := NewUnionFind(g.vertices)

    for u := 0; u < g.vertices; u++ {
        for _, v := range g.adjList[u] {
            if u < v { // To ensure each edge is processed once
                if uf.Find(u) == uf.Find(v) {
                    return true
                }
                uf.Union(u, v)
            }
        }
    }
    return false
}

// UnionFind represents the Union-Find (Disjoint Set) data structure
type UnionFind struct {
    parent []int
    rank   []int
}

// NewUnionFind initializes a new Union-Find structure
func NewUnionFind(size int) *UnionFind {
    uf := &UnionFind{
        parent: make([]int, size),
        rank:   make([]int, size),
    }
    for i := 0; i < size; i++ {
        uf.parent[i] = i
        uf.rank[i] = 0
    }
    return uf
}

// Find finds the root of the set in which element k is present
func (uf *UnionFind) Find(k int) int {
    if uf.parent[k] != k {
        uf.parent[k] = uf.Find(uf.parent[k]) // Path compression
    }
    return uf.parent[k]
}

// Union unites the sets that include x and y
func (uf *UnionFind) Union(x, y int) {
    xroot := uf.Find(x)
    yroot := uf.Find(y)

    if xroot == yroot {
        return
    }

    // Union by rank
    if uf.rank[xroot] < uf.rank[yroot] { uf.parent[xroot] = yroot } else if uf.rank[xroot] > uf.rank[yroot] {
        uf.parent[yroot] = xroot
    } else {
        uf.parent[yroot] = xroot
        uf.rank[xroot]++
    }
}

func main() {
    // Example usage for a Directed Graph
    fmt.Println("Directed Graph Cycle Detection using DFS:")
    dg := NewGraph(4)
    dg.AddEdge(0, 1, true)
    dg.AddEdge(1, 2, true)
    dg.AddEdge(2, 3, true)
    dg.AddEdge(3, 1, true) // Creates a cycle

    if dg.CycleDetectedDFS() {
        fmt.Println("Cycle detected in the directed graph.")
    } else {
        fmt.Println("No cycle detected in the directed graph.")
    }

    // Example usage for an Undirected Graph
    fmt.Println("\nUndirected Graph Cycle Detection using DFS:")
    ug := NewGraph(4)
    ug.AddEdge(0, 1, false)
    ug.AddEdge(1, 2, false)
    ug.AddEdge(2, 3, false)
    ug.AddEdge(3, 1, false) // Creates a cycle

    if ug.CycleDetectedDFSUndirected() {
        fmt.Println("Cycle detected in the undirected graph using DFS.")
    } else {
        fmt.Println("No cycle detected in the undirected graph using DFS.")
    }

    // Example usage for an Undirected Graph using Union-Find
    fmt.Println("\nUndirected Graph Cycle Detection using Union-Find:")
    if ug.CycleDetectedUnionFind() {
        fmt.Println("Cycle detected in the undirected graph using Union-Find.")
    } else {
        fmt.Println("No cycle detected in the undirected graph using Union-Find.")
    }

    // Example usage for an Undirected Graph without a cycle
    fmt.Println("\nUndirected Graph Cycle Detection without a cycle:")
    ug2 := NewGraph(3)
    ug2.AddEdge(0, 1, false)
    ug2.AddEdge(1, 2, false)

    if ug2.CycleDetectedDFSUndirected() {
        fmt.Println("Cycle detected in the undirected graph using DFS.")
    } else {
        fmt.Println("No cycle detected in the undirected graph using DFS.")
    }

    if ug2.CycleDetectedUnionFind() {
        fmt.Println("Cycle detected in the undirected graph using Union-Find.")
    } else {
        fmt.Println("No cycle detected in the undirected graph using Union-Find.")
    }
}
    

Documentation

The Go program provided implements cycle detection in both directed and undirected graphs using two different algorithms: Depth-First Search (DFS) and Union-Find (Disjoint Set). Below is a detailed explanation of each component of the program:

1. Graph Representation

The Graph struct represents a graph using an adjacency list. The adjacency list is a slice of slices, where each sub-slice contains the adjacent vertices for a given vertex.

NewGraph Function

The NewGraph function initializes a new graph with a specified number of vertices. It creates an empty adjacency list for each vertex.

AddEdge Method

The AddEdge method adds a directed or undirected edge between two vertices. For undirected graphs, the edge is added in both directions to ensure bidirectional connectivity.

2. Depth-First Search (DFS) Based Cycle Detection

DFS is a traversal technique that explores as far as possible along each branch before backtracking. It is used to detect cycles by keeping track of the recursion stack (vertices currently in the traversal path).

CycleDetectedDFS Method (Directed Graph)

The CycleDetectedDFS method detects cycles in a directed graph. It uses two boolean slices: visited to track visited vertices and recStack to track the recursion stack. If a vertex is encountered that is already in the recursion stack, a cycle is detected.

dfsCycle Helper Function

The dfsCycle function is a recursive helper that performs DFS traversal. It marks the current vertex as visited and adds it to the recursion stack. It then recursively visits all adjacent vertices. If it encounters a vertex already in the recursion stack, it returns true, indicating a cycle.

CycleDetectedDFSUndirected Method (Undirected Graph)

The CycleDetectedDFSUndirected method detects cycles in an undirected graph. It uses a similar approach to the directed version but additionally keeps track of the parent vertex to avoid false-positive cycle detection due to bidirectional edges.

dfsCycleUndirected Helper Function

The dfsCycleUndirected function is a recursive helper for undirected graphs. It marks the current vertex as visited and recursively visits all adjacent vertices. If it encounters an already visited vertex that is not the parent, it returns true, indicating a cycle.

3. Union-Find (Disjoint Set) Based Cycle Detection

The Union-Find data structure efficiently tracks the components of the graph and detects cycles by checking if two vertices belong to the same set.

UnionFind Struct

The UnionFind struct maintains two slices: parent to store the parent of each vertex and rank to store the rank (depth) of each tree.

NewUnionFind Function

The NewUnionFind function initializes the Union-Find structure. Each vertex is initially its own parent, and ranks are set to 0.

Find Method

The Find method recursively finds the root parent of a given vertex. It implements path compression by updating the parent of each visited vertex to the root, optimizing future searches.

Union Method

The Union method unites two sets containing vertices x and y. It attaches the tree with a lower rank to the tree with a higher rank. If both trees have the same rank, one becomes the parent of the other, and its rank is incremented.

CycleDetectedUnionFind Method

The CycleDetectedUnionFind method detects cycles in an undirected graph using the Union-Find algorithm. It iterates through all edges, performing union operations. If two vertices of an edge already belong to the same set, a cycle is detected.

4. Main Function

The main function demonstrates the usage of the cycle detection methods:

  1. Directed Graph Example:
    • A directed graph with a cycle is created.
    • Cycle detection is performed using the DFS-based method.
    • The result is printed.
  2. Undirected Graph Example with Cycle:
    • An undirected graph with a cycle is created.
    • Cycle detection is performed using both DFS-based and Union-Find-based methods.
    • The results are printed.
  3. Undirected Graph Example without Cycle:
    • An undirected graph without a cycle is created.
    • Cycle detection is performed using both DFS-based and Union-Find-based methods.
    • The results are printed.

How the Program Works

  1. Graph Initialization: The program starts by creating graphs using the NewGraph function, specifying the number of vertices.
  2. Adding Edges: Edges are added to the graph using the AddEdge method. For undirected graphs, edges are added in both directions to ensure bidirectional connectivity.
  3. Cycle Detection (Directed Graph using DFS):
    • The program initializes two boolean slices: visited and recStack.
    • It performs DFS traversal starting from each unvisited vertex.
    • If a vertex is encountered that is already in the recursion stack, a cycle is detected.
    • The result is printed based on whether a cycle was found.
  4. Cycle Detection (Undirected Graph using DFS):
    • The program initializes a visited slice.
    • It performs DFS traversal from each unvisited vertex, keeping track of the parent to avoid false positives.
    • If an already visited vertex that is not the parent is encountered, a cycle is detected.
    • The result is printed based on whether a cycle was found.
  5. Cycle Detection (Undirected Graph using Union-Find):
    • The program initializes a Union-Find structure.
    • It iterates through all edges, performing union operations.
    • If two vertices of an edge are already in the same set, a cycle is detected.
    • The result is printed based on whether a cycle was found.
  6. Undirected Graph without a Cycle:
    • An undirected graph without any cycles is created.
    • Cycle detection is performed using both DFS-based and Union-Find-based methods.
    • The results confirm the absence of cycles.

Example Output

Running the program produces the following output:


Directed Graph Cycle Detection using DFS:
Cycle detected in the directed graph.

Undirected Graph Cycle Detection using DFS:
Cycle detected in the undirected graph using DFS.
Cycle detected in the undirected graph using Union-Find.

Undirected Graph Cycle Detection without a cycle:
No cycle detected in the undirected graph using DFS.
No cycle detected in the undirected graph using Union-Find.
    

This output demonstrates the detection of cycles in both directed and undirected graphs using different algorithms, as well as confirming the absence of cycles in an acyclic graph.

Conclusion

Cycle detection is a critical aspect of graph analysis, enabling the identification of structural anomalies and dependencies within a graph. This Go program provides robust implementations for detecting cycles in both directed and undirected graphs using DFS and Union-Find algorithms. By understanding and utilizing these methods, developers can effectively analyze and manage graph-based data structures in various applications, ensuring system reliability and integrity.

Excerpt

Learn how to detect cycles in directed and undirected graphs using Go. 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 :)