Does a directed graph contain a cycle?
A directed graph has n nodes numbered 0 to n − 1, and each pair
[u, v] in edges is an edge from u to v. No edge is repeated and
none joins a node to itself. Return true if some sequence of edges leads from a node back to itself,
and false otherwise.
Examples
| Input | Output | Why |
|---|---|---|
n = 6, edges = [[0,1],[0,2],[2,3],[1,3],[3,4],[4,5],[5,3]] | true | 3 → 4 → 5 → 3. |
n = 4, edges = [[0,1],[0,2],[1,3],[2,3]] | false | Two routes to 3, but no way back from it. |
n = 2, edges = [[0,1],[1,0]] | true | Two opposite edges form a cycle in a directed graph. |
Constraints
- 1 ≤ n ≤ 2000
- 0 ≤ edges.length ≤ 5000
- No self-loops and no repeated edges
Hints
Hint 1
A visited node is not enough evidence here: the diamond in example 2 reaches 3 twice without a cycle.
Hint 2
Track three states per node: unvisited, on the current path, finished.
Hint 3
An edge into a node that is on the current path closes a cycle. An edge into a finished node never does.
Where people lose marks
- Reporting a cycle on any visited node, which flags every diamond.
- Forgetting to colour a node black when its search finishes, so it stays grey and causes false alarms later.
- Searching only from node 0: a cycle may be unreachable from it.
Dry run on 6,[[0,1],[0,2],[2,3],[1,3],[3,4],[4,5],[5,3]]
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
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.
How to think about it
Search depth-first and colour each node white (unvisited), grey (entered, still on the current path) or black (finished). An edge from the current node to a grey node points back along the path the search is on, so following it and then the path returns to the start: a cycle. An edge to a black node does not: everything reachable from a black node was explored while it was grey, and no route back to the path was found.
Without the grey state, an edge into any visited node would count, and a diamond (two routes to the same node) would be reported as a cycle.
Complexity. Time O(n + m) · Space O(n + m)
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
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
Variants to try
- Course schedule: can all courses be taken, given prerequisite pairs? It is this problem, with edges from prerequisite to course.
- Return the nodes of one cycle by keeping the current path on a stack.
- Find every node that cannot reach a cycle (the "eventually safe" nodes): exactly the ones that end black.
Next problems
Does an undirected graph contain a cycle?
Return true if the graph has a cycle: a search that meets a visited node by any edge but the one it came along has found one.
Open the problem → Easy12 minCount the islands in a grid
Every unvisited land cell starts a new island; flood it so it is never counted again. The first graph problem most interviews ask.
Open the problem → Medium15 minCan the graph be split in two?
Return true if the nodes can be coloured with two colours so that every edge joins different colours. Colour while you search.
Open the problem →Write a solution and run it against the real test table.
Keep going
Does an undirected graph contain a cycle?
Return true if the graph has a cycle: a search that meets a visited node by any edge but the one it came along has found one.
Open the problem → Easy12 minCount the islands in a grid
Every unvisited land cell starts a new island; flood it so it is never counted again. The first graph problem most interviews ask.
Open the problem → Medium15 minCan the graph be split in two?
Return true if the nodes can be coloured with two colours so that every edge joins different colours. Colour while you search.
Open the problem → GraphsThe pattern behind it
Grids, networks and dependencies are all graphs. Breadth-first search finds shortest paths, depth-first search and union-find find what is connected, Dijkstra handles weighted edges, and a queue of nodes with no remaining prerequisites gives an order.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.