Skip to content
dsa450for five year olds

Graph · 4 of 43

Detect Cycle in Directed Graph using BFS DFS Algo

Follow one-way arrows between rooms and find out if some path of arrows leads you back where you started.

Simple way
time O(V * (V + E))
room O(V)
Clever way
time O(V + E)
room O(V)

The problem, simply

You get a directed graph: V rooms and one-way arrows between them. A cycle is a path that follows arrows and comes back to a room it already passed.

Return true if the graph has at least one cycle, and false if it has none.

A story to remember it

Picture it

graph
012345

Rooms 2, 4 and 5 form a loop of one-way arrows. Room 3 can be reached two ways, from 1 and from 2, but that is not a loop.

  • rejected

Examples

A loop at the end

In
V = 4, edges = [[0,1],[1,2],[2,3],[3,1]]
Out
true

1 to 2 to 3 and back to 1.

Two roads to one room

In
V = 4, edges = [[0,1],[0,2],[1,3],[2,3]]
Out
false

Room 3 is reached twice, but you can never leave 3 and come back to 0. Seeing a room twice is not the same as a cycle.

A room pointing at itself

In
V = 1, edges = [[0,0]]
Out
true

A self-loop is a cycle of length one.

No arrows

In
V = 3, edges = []
Out
false

Nothing to follow, so nothing can loop.

Way 1: the simple way

For every room, ask: can I leave this room and come back to it? Start a search from each room's neighbors and see if it reaches the room again.

  1. For each room s, put its neighbors in a waiting line and mark them seen.
  2. Search outward. If you ever reach s, return true.
  3. If no room can get back to itself, return false.
java
class DirectedCycleBrute {
    public static boolean hasCycle(int V, List<List<Integer>> adj) {
        for (int s = 0; s < V; s++) {
            boolean[] seen = new boolean[V];
            Deque<Integer> line = new ArrayDeque<>();
            for (int v : adj.get(s)) if (!seen[v]) { seen[v] = true; line.add(v); }
            while (!line.isEmpty()) {
                int u = line.poll();
                if (u == s) return true;                  // walked back home
                for (int v : adj.get(u)) {
                    if (!seen[v]) { seen[v] = true; line.add(v); }
                }
            }
        }
        return false;
    }
}

How long it takes: one search per room, each O(V + E), so O(V * (V + E)). Extra room: the seen array and the line, O(V).

Way 2: the clever way

One depth first walk, with three kinds of rooms: new, on the walk (the string is in it), and done.

  1. Mark the room "on the walk" when you step in.
  2. Try each arrow out. If it leads to a room that is on the walk, return true. If it leads to a new room, walk into it. If it leads to a done room, skip it.
  3. When all arrows are tried, mark the room "done" and step back.
  4. Start a walk from every room that is still new.

The string shows the current walk, stickers show finished rooms

01 / 10

graph
012345
arraystate
012345on walknewnewnewnewnew
stackcurrent walk
dfs(0)top

Start at 0. Mark it "on the walk".

  • looking now
  • checking
  • done
  • rejected
  • path
  • just added
java
class DirectedCycleDfs {
    static final int NEW = 0, ON_WALK = 1, DONE = 2;

    public static boolean hasCycle(int V, List<List<Integer>> adj) {
        int[] state = new int[V];
        for (int i = 0; i < V; i++) {
            if (state[i] == NEW && walk(i, adj, state)) return true;
        }
        return false;
    }

    static boolean walk(int u, List<List<Integer>> adj, int[] state) {
        state[u] = ON_WALK;                               // the string is in this room
        for (int v : adj.get(u)) {
            if (state[v] == ON_WALK) return true;         // back into our own path
            if (state[v] == NEW && walk(v, adj, state)) return true;
        }
        state[u] = DONE;                                  // roll the string back
        return false;
    }
}

How long it takes: every room is stepped into once and every arrow is tried once, O(V + E). Extra room: the state array and the call stack, O(V).

Another way: peel off rooms with no arrows in

This is Kahn's way, with a waiting line, a queue. A room with no arrows coming in can be handled first. Remove it and its arrows, and new rooms may lose their last incoming arrow. If the graph has no cycle, you can peel off every room. Rooms on a cycle always keep one arrow coming in, so they never get peeled.

  1. Count the arrows coming into each room.
  2. Put every room with count 0 in the line.
  3. Take a room out, count it, and lower the count of each room it points to. Any that hit 0 join the line.
  4. If you counted fewer than V rooms, there is a cycle.
graph
0123451 in1 in1 in1 in
queueline is empty
outinempty

On our example: 0 is peeled, then 1. Room 2 still has an arrow from 5, and 5 waits on 4, which waits on 2. Only 2 of 6 rooms get peeled, so there is a cycle.

peeled
2
rooms
6
  • done
  • rejected
  • ignored
java
class DirectedCycleKahn {
    public static boolean hasCycle(int V, List<List<Integer>> adj) {
        int[] arrowsIn = new int[V];
        for (int u = 0; u < V; u++) for (int v : adj.get(u)) arrowsIn[v]++;
        Deque<Integer> line = new ArrayDeque<>();
        for (int i = 0; i < V; i++) if (arrowsIn[i] == 0) line.add(i);
        int peeled = 0;
        while (!line.isEmpty()) {
            int u = line.poll();
            peeled++;
            for (int v : adj.get(u)) {
                if (--arrowsIn[v] == 0) line.add(v);      // its last arrow in is gone
            }
        }
        return peeled < V;                                // someone was stuck in a loop
    }
}

Same O(V + E) time and O(V) extra room, and no recursion, so it is safe for very deep graphs.

Watch out for

  • Forgetting to mark a room "done" when you step back. Then it stays "on the walk" forever and causes false alarms later.
  • Starting only from room 0. A loop that 0 cannot reach is missed. Loop over all rooms.
  • Self-loops: the DFS sees state[u] == ON_WALK for v == u and correctly says true.

What interviewers ask next

  • How do you print the cycle? Keep parent[v] = u when you walk in. When you find the arrow u to v with v on the walk, follow parents from u back to v.
  • Can you finish all courses with these prerequisites? That is this exact question: possible only if there is no cycle.
  • Give a valid order of tasks. Kahn's peel order is a topological order. With DFS, the reverse of the "done" order is one too.
  • Why does the undirected parent trick not work here? In a directed graph, an arrow back to an already seen room may lead to a finished side branch, not to your own path. You must know which rooms are still on the path.

Remember this

Grown-up words: rooms are vertices, one-way doors are directed edges, the string is the recursion stack, the three states are often called white, gray and black, a "done" sticker is a fully explored vertex, peeling rooms with no arrows in is Kahn's algorithm, and the count of arrows in is the in-degree.