Cycles in a directed graph
A visited node is not evidence of a cycle in a directed graph. A node still on the current path is: three colours tell them apart.
Why "visited" is not enough
The undirected test (a visited node other than the parent means a cycle) gives false alarms on a directed graph. Take a diamond: 0 points to 1 and 2, and both point to 3. Searching from 0 through 1 visits 3. Coming back and going through 2, the search meets 3 again, already visited, yet there is no cycle: there is no way back from 3 to anything.
What a cycle needs is an edge back to a node that is on the current path: an ancestor that the search has entered and not yet left. Following that edge leads back to where the path began.
Three colours
Give every node one of three states:
- White: not visited yet.
- Grey: entered, not finished. The grey nodes are exactly the current path from the start to here.
- Black: finished. Everything reachable from it has been explored, and none of it led back to the path.
An edge to a grey node closes a cycle. An edge to a black node is safe to ignore: if a cycle ran through a black node, the search would have found it while that node was grey.
Cycle in a directed graph
visit(u)
- Colour
ugrey. - For each
vwith an edgeu → v: ifvis grey, return true; ifvis white andvisit(v)is true, return true. - Colour
ublack and return false.
main
- For every white node
s: ifvisit(s), return true. - Return false.
Step by step
A graph with a cycle 3 → 4 → 5 → 3, reached from 0 along two different routes:
Grey nodes form the current path; the search stops at the first edge into a grey node.
-
Step 1 / 6grey: the current path00
Colour 0 grey: it is on the current path.
-
Step 2 / 6grey: the current path0011
Colour 1 grey: it is on the current path.
-
Step 3 / 6grey: the current path001132
Colour 3 grey: it is on the current path.
-
Step 4 / 6grey: the current path00113243
Colour 4 grey: it is on the current path.
-
Step 5 / 6grey: the current path0011324354
Colour 5 grey: it is on the current path.
-
Step 6 / 6grey: the current path0011324354
5 → 3 reaches a grey node, one still on the current path. The path from 3 to 5 plus this edge is a cycle.
current nodegrey: on the current pathblack: finishededge into a grey node
Implementation
Three states in an integer array: 0 white, 1 grey, 2 black.
import sys
def has_directed_cycle(n, edges):
sys.setrecursionlimit(10000)
adj = [[] for _ in range(n)]
for u, v in edges:
adj[u].append(v)
WHITE, GREY, BLACK = 0, 1, 2
colour = [WHITE] * n
def visit(u):
colour[u] = GREY # u is on the current path
for v in adj[u]:
if colour[v] == GREY:
return True # back along the path: a cycle
if colour[v] == WHITE and visit(v):
return True
colour[u] = BLACK # finished, and no cycle through it
return False
return any(colour[s] == WHITE and visit(s) for s in range(n))#include <functional>
#include <vector>
bool solve(int n, const std::vector<std::vector<int>>& edges) {
std::vector<std::vector<int>> adj(n);
for (const auto& e : edges) adj[e[0]].push_back(e[1]);
enum { WHITE, GREY, BLACK };
std::vector<int> colour(n, WHITE);
std::function<bool(int)> visit = [&](int u) {
colour[u] = GREY; // u is on the current path
for (int v : adj[u]) {
if (colour[v] == GREY) return true; // a cycle
if (colour[v] == WHITE && visit(v)) return true;
}
colour[u] = BLACK; // finished
return false;
};
for (int s = 0; s < n; ++s)
if (colour[s] == WHITE && visit(s)) return true;
return false;
}import java.util.*;
class Solution {
private List<List<Integer>> adj;
private int[] colour; // 0 white, 1 grey, 2 black
public boolean hasDirectedCycle(int n, int[][] edges) {
adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (int[] e : edges) adj.get(e[0]).add(e[1]);
colour = new int[n];
for (int s = 0; s < n; s++)
if (colour[s] == 0 && visit(s)) return true;
return false;
}
private boolean visit(int u) {
colour[u] = 1; // u is on the current path
for (int v : adj.get(u)) {
if (colour[v] == 1) return true; // a cycle
if (colour[v] == 0 && visit(v)) return true;
}
colour[u] = 2; // finished
return false;
}
}function hasDirectedCycle(n, edges) {
const adj = Array.from({ length: n }, () => []);
for (const [u, v] of edges) adj[u].push(v);
const WHITE = 0, GREY = 1, BLACK = 2;
const colour = new Array(n).fill(WHITE);
const visit = u => {
colour[u] = GREY; // u is on the current path
for (const v of adj[u]) {
if (colour[v] === GREY) return true; // a cycle
if (colour[v] === WHITE && visit(v)) return true;
}
colour[u] = BLACK; // finished, and no cycle through it
return false;
};
for (let s = 0; s < n; s++) if (colour[s] === WHITE && visit(s)) return true;
return false;
}fn solve(n: i64, edges: &[[i64; 2]]) -> bool {
let n = n as usize;
let mut adj = vec![Vec::new(); n];
for e in edges {
adj[e[0] as usize].push(e[1] as usize);
}
// 0 white, 1 grey (on the current path), 2 black (finished)
fn visit(u: usize, adj: &[Vec<usize>], colour: &mut Vec<u8>) -> bool {
colour[u] = 1;
for &v in &adj[u] {
if colour[v] == 1 {
return true; // a cycle
}
if colour[v] == 0 && visit(v, adj, colour) {
return true;
}
}
colour[u] = 2;
false
}
let mut colour = vec![0u8; n];
(0..n).any(|s| colour[s] == 0 && visit(s, &adj, &mut colour))
}import qualified Data.Map.Strict as M
data Colour = Grey | Black deriving Eq
solve :: Int -> [[Int]] -> Bool
solve n edges = go [0 .. n - 1] M.empty
where
adj = M.fromListWith (flip (++)) [(u, [v]) | [u, v] <- edges]
-- colours absent from the map are white
go [] _ = False
go (s : rest) col
| M.member s col = go rest col
| otherwise = case visit s col of
Nothing -> True
Just col' -> go rest col'
-- Nothing means a cycle; Just carries the colours on
visit u col = fmap (M.insert u Black) (foldr step (Just (M.insert u Grey col)) (reverse (M.findWithDefault [] u adj)))
where
step _ Nothing = Nothing
step v (Just c) = case M.lookup v c of
Just Grey -> Nothing
Just Black -> Just c
Nothing -> visit v c
The same code is the reference solution to Does a directed graph contain a cycle?, where it is compiled and run against the tests in all six languages.
Each node changes colour twice and each edge is examined once. The recursion can be n deep; for very long chains, use the in-degree method from the next module, which needs no recursion.
The order in which the search colours nodes black is useful in its own right: reversed, it is a topological order of the graph. The next module builds on exactly that.