Detect cycle in 3 Ways

Check if a graph contains a cycle in 3 ways #

Cycle detection is an application that is widely used in deadlock detection, wait-for graph, or dependencies analysis, this post is implementing cycle detection in 3 ways.

Introduction #

Given a connected undirected graph, check if it contains any cycle or not.

For example, the following graph contains a cycle 2-5-10-6-2:

Cyclic-Breadth-first-tree

1. Using BFS #

When we do a Breadth–first search (BFS) from any vertex v in an undirected graph, we may encounter a cross-edge that points to a previously discovered vertex that is neither an ancestor nor a descendant of the current vertex. Each “cross edge” defines a cycle in an undirected graph. If the cross edge is x —> y and y is already discovered, we have a path from v to y (or from y to v since the graph is undirected), where v is the starting vertex of BFS. So, we can say that we have a path v ~~ x ~ y ~~ v that forms a cycle. (Here, ~~ represents one more edge in the path, and ~ represents a direct edge).

#include <iostream>
#include <vector>
#include <queue>
using namespace std;
 
// Data structure to store a graph edge
struct Edge {
    int src, dest;
};
 
// A class to represent a graph object
class Graph
{
public:
    // a vector of vectors to represent an adjacency list
    vector<vector<int>> adjList;
 
    // Graph Constructor
    Graph(vector<Edge> const &edges, int n)
    {
        // resize the vector to hold `n` elements of type `vector<int>`
        adjList.resize(n);
 
        // add edges to the undirected graph
        for (auto &edge: edges)
        {
            adjList[edge.src].push_back(edge.dest);
            adjList[edge.dest].push_back(edge.src);
        }
    }
};
 
// Node to store vertex and its parent info in BFS
struct Node {
    int v, parent;
};
 
// Perform BFS on the graph starting from vertex `src` and
// return true if a cycle is found in the graph
bool BFS(Graph const &graph, int src, int n)
{
    // to keep track of whether a vertex is discovered or not
    vector<bool> discovered(n);
 
    // mark the source vertex as discovered
    discovered[src] = true;
 
    // create a queue for doing BFS and
    // enqueue source vertex
    queue<Node> q;
    q.push({src, -1});
 
    // loop till queue is empty
    while (!q.empty())
    {
        // dequeue front node and print it
        Node node = q.front();
        q.pop();
 
        // do for every edge (v, u)
        for (int u: graph.adjList[node.v])
        {
            if (!discovered[u])
            {
                // mark it as discovered
                discovered[u] = true;
 
                // construct the queue node containing info
                // about vertex and enqueue it
                q.push({ u, node.v });
            }
 
            // `u` is discovered, and `u` is not a parent
            else if (u != node.parent)
            {
                // we found a cross-edge, i.e., the cycle is found
                return true;
            }
        }
    }
 
    // no cross-edges were found in the graph
    return false;
}
 
int main()
{
    // initialize edges
    vector<Edge> edges =
    {
        {0, 1}, {0, 2}, {0, 3}, {1, 4}, {1, 5}, {4, 8},
        {4, 9}, {3, 6}, {3, 7}, {6, 10}, {6, 11}, {5, 9}
        // edge {5, 9} introduces a cycle in the graph
    };
 
    // total number of nodes in the graph (0 to 11)
    int n = 12;
 
    // build a graph from the given edges
    Graph graph(edges, n);
 
    // Perform BFS traversal in connected components of a graph
    if (BFS(graph, 0, n)) {
        cout << "The graph contains a cycle";
    }
    else {
        cout << "The graph doesn't contain any cycle";
    }
 
    return 0;
}
Output: The graph contains a cycle

2. Using DFS #

The following graph contains a cycle 8—9—11—12—8:

Cyclic-Depth-first-tree

When we do a Depth–first search (DFS) from any vertex v in an undirected graph, we may encounter a back-edge that points to one of the ancestors of the current vertex v in the DFS tree. Each “back edge” defines a cycle in an undirected graph. If the back edge is x —> y, and y is the ancestor of node x, we have a path from y to x. So, we can say that we have a path y ~~ x ~ y that forms a cycle. (Here, ~~ represents one more edge in the path, and ~ represents a direct edge).

#include <iostream>
#include <vector>
#include <queue>
using namespace std;
 
// Data structure to store a graph edge
struct Edge {
    int src, dest;
};
 
// A class to represent a graph object
class Graph
{
public:
 
    // a vector of vectors to represent an adjacency list
    vector<vector<int>> adjList;
 
    // Graph Constructor
    Graph(vector<Edge> const &edges, int n)
    {
        // resize the vector to hold `n` elements of type `vector<int>`
        adjList.resize(n);
 
        // add edges to the undirected graph
        for (auto &edge: edges)
        {
            adjList[edge.src].push_back(edge.dest);
            adjList[edge.dest].push_back(edge.src);
        }
    }
};
 
// Perform DFS on the graph and returns true if any back-edge is found in the graph
bool DFS(Graph const &graph, int v, vector<bool> &discovered, int parent)
{
    // mark the current node as discovered
    discovered[v] = true;
 
    // do for every edge (v, w)
    for (int w: graph.adjList[v])
    {
        // if `w` is not discovered
        if (!discovered[w])
        {
            if (DFS(graph, w, discovered, v)) {
                return true;
            }
        }
 
        // if `w` is discovered, and `w` is not a parent
        else if (w != parent)
        {
            // we found a back-edge (cycle)
            return true;
        }
    }
 
    // No back-edges were found in the graph
    return false;
}
 
int main()
{
    // initialize edges
    vector<Edge> edges =
    {
        {0, 1}, {0, 6}, {0, 7}, {1, 2}, {1, 5}, {2, 3},
        {2, 4}, {7, 8}, {7, 11}, {8, 9}, {8, 10}, {10, 11}
        // edge (10, 11) introduces a cycle in the graph
    };
 
    // total number of nodes in the graph (0 to 11)
    int n = 12;
 
    // build a graph from the given edges
    Graph graph(edges, n);
 
    // to keep track of whether a vertex is discovered or not
    vector<bool> discovered(n);
 
    // Perform DFS traversal from the first vertex
    if (DFS(graph, 0, discovered, -1)) {
        cout << "The graph contains a cycle";
    }
    else {
        cout << "The graph doesn't contain any cycle";
    }
 
    return 0;
}
Output: The graph contains a cycle

The time complexity of the above solutions is O(V + E), where V and E are the total number of vertices and edges in the graph, respectively.

3. Using Disjoint-Set(Uniou-Find Algorithm) #

The following graph contains a cycle 8—9—11—12—8:

Cyclic-Depth-first-tree

We strongly recommend watch the first 10 mins of this video to get a clear picture.

Complete Algorithm:

1. Create disjoint sets for each vertex of the graph.
2. For every edge u, v in the graph
    i) Find the root of the sets to which elements u and v belongs.
    ii) If both u and v have the same root in disjoint sets, a cycle is found.
#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;
 
// Data structure to store a graph edge
struct Edge {
    int src, dest;
};
 
// A class to represent a graph object
class Graph
{
public:
    // a vector of vectors to represent an adjacency list
    vector<vector<int>> adjList;
 
    // Graph Constructor
    Graph(vector<Edge> const &edges, int n)
    {
        // resize the vector to hold `n` elements of type `vector<int>`
        adjList.resize(n);
 
        // add edges to the undirected graph
        for (auto &edge: edges) {
            adjList[edge.src].push_back(edge.dest);
            adjList[edge.dest].push_back(edge.src);
        }
    }
};
 
// A class to represent a disjoint set
class DisjointSet
{
    unordered_map<int, int> parent;
public:
    // perform MakeSet operation
    void MakeSet(int n)
    {
        // create `n` disjoint sets (one for each vertex)
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
    }
 
    // Find the root of the set in which element `k` belongs
    int Find(int k)
    {
        // if `k` is root
        if (parent[k] == k) {
            return k;
        }
 
        // recur for the parent until we find the root
        return Find(parent[k]);
    }
 
    // Perform Union of two subsets
    void Union(int a, int b)
    {
        // find the root of the sets in which elements `x` and `y` belongs
        int x = Find(a);
        int y = Find(b);
 
        parent[x] = y;
    }
};
 
// Returns true if the graph has a cycle
bool findCycle(Graph const &graph, int n)
{
    // initialize Main class
    DisjointSet ds;
 
    // create a singleton set for each element of the universe
    ds.MakeSet(n);


    // to keep track of whether a vertex is discovered or not
    vector<bool> discovered(n);
 
    // consider every edge (u, v)
    for (int u = 0; u < n; u++)
    {
        // Recur for all adjacent vertices
        for (int v: graph.adjList[u])
        {
            if (discovered[v]) {
                continue;
            }
            // find the root of the sets to which elements `u` and `v` belongs
            int x = ds.Find(u);
            int y = ds.Find(v);
 
            // if both `u` and `v` have the same parent, the cycle is found
            if (x == y) {
                return true;
            }
            ds.Union(x, y);
        }
        discovered[u] = true;
    }
 
    return false;
}
 
// Union–find algorithm for cycle detection in a graph
int main()
{
    // vector of graph edges
    vector<Edge> edges =
    {
        {0, 1}, {0, 6}, {0, 7}, {1, 2}, {1, 5}, {2, 3},
        {2, 4}, {7, 8}, {7, 11}, {8, 9}, {8, 10}, {10, 11}
        // edge (10, 11) introduces a cycle in the graph
    };
 
    // total number of nodes in the graph (labelled from 0 to 11)
    int n = 12;
 
    // build a graph from the given edges
    Graph graph(edges, n);
 
    if (findCycle(graph, n)) {
        cout << "Cycle Found";
    }
    else {
        cout << "No Cycle Found";
    }
 
    return 0;
}
Output: Cycle Found

The time complexity of the Union and Find operation is O(n) in the worst case, where n is the total number of vertices in the graph.

Reference #