Skip to content
DSA graphs 8 min read

Cycle Detection in Graphs: Undirected & Directed

A cycle is a path that returns to where it started. Detecting cycles tells you whether a dependency graph is resolvable, whether a graph is a tree, or whether a schedule is feasible. The technique differs by graph type: undirected graphs use union-find or DFS with a parent check, while directed graphs use a three-color DFS that distinguishes “still on the stack” from “fully done.”

Undirected graphs with union-find

In an undirected graph, process each edge (u, v). If u and v are already in the same set, adding this edge closes a loop — a cycle. Otherwise union them. This is union-find (disjoint set union), the same structure behind Kruskal’s MST.

struct DSU {
    std::vector<int> parent, rank_;
    DSU(int n): parent(n), rank_(n, 0) { for (int i=0;i<n;i++) parent[i]=i; }
    int find(int x){ return parent[x]==x ? x : parent[x]=find(parent[x]); }
    bool unite(int a,int b){
        a=find(a); b=find(b);
        if(a==b) return false;            // already connected -> cycle
        if(rank_[a]<rank_[b]) std::swap(a,b);
        parent[b]=a;
        if(rank_[a]==rank_[b]) rank_[a]++;
        return true;
    }
};
bool hasCycleUndirected(int V, const std::vector<std::pair<int,int>>& edges){
    DSU dsu(V);
    for (auto [u,v] : edges) if(!dsu.unite(u,v)) return true;
    return false;
}

For beginners: An undirected DFS alternative also works — run DFS, and if you reach an already-visited neighbor that is not the vertex you came from (the parent), you’ve found a cycle.

Directed graphs with three colors

In a directed graph, returning to a visited vertex is not automatically a cycle — it might just be a second path to a finished vertex. The fix is three colors:

  • White (0): unvisited.
  • Gray (1): on the current DFS path (in the recursion stack).
  • Black (2): fully explored, all descendants done.

A gray vertex reached again means a back edge — a true cycle.

bool dfsDir(int u, const std::vector<std::vector<int>>& adj, std::vector<int>& color){
    color[u] = 1;                          // gray: on current path
    for (int v : adj[u]) {
        if (color[v] == 1) return true;    // back edge -> cycle
        if (color[v] == 0 && dfsDir(v, adj, color)) return true;
    }
    color[u] = 2;                          // black: done
    return false;
}
bool hasCycleDirected(const std::vector<std::vector<int>>& adj){
    std::vector<int> color(adj.size(), 0);
    for (int i = 0; i < (int)adj.size(); i++)
        if (color[i] == 0 && dfsDir(i, adj, color)) return true;
    return false;
}

Pitfall: Using a single boolean visited array for a directed graph gives false positives — a vertex reachable by two separate paths looks like a cycle. You need the gray/black distinction (or an explicit “in current recursion stack” set).

Complexity

Both approaches run in O(V + E) time. Union-find uses O(V) space; the color DFS uses O(V) for the color array plus recursion stack. The near-O(1) amortized union-find operations come from path compression and union by rank.

A directed graph that has no cycle is a DAG, which you can linearize with topological sort — coming up next.

Last updated June 25, 2026
Was this helpful?