Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Graph Algorithms

Builds on Flood Fill, which you haven’t finished yet.

P2MediumBuilt on Flood Fill~6 min speedrun

Cycle Detection in a Directed Graph (DFS Three-Color)

Built on
Flood FillA node has three states, not seen, on the current path and finished, and reaching a node on the current path proves a cycle.
Grows into
Strongly Connected Components
Unlocks
Tarjan's Bridges
The problem

Course Schedule (LC 207)(opens leetcode.com)

You have numCourses courses to complete, numbered 0 through numCourses - 1, and a list prerequisites where each entry [ai, bi] means course bi must be completed before course ai.

  • So an entry [0, 1] means course 1 has to be finished before course 0.

Decide whether every course can be completed at all — return true if a valid completion order exists, and false otherwise.

Example 1
Input:numCourses = 2, prerequisites = [[1,0]]
Output:true
With 2 courses total, course 1's only requirement is course 0, so taking course 0 then course 1 completes both.

Cycle Detection

WHITE (0=unvisited) ➔ GREY (1=visiting) ➔ BLACK (2=processed)
Spot it when: You must detect a cycle in a directed graph, find the nodes that can't reach a cycle (safe nodes), or check that every path ends properly.
Cycle Detection
Python
1from typing import List
2 
3def has_cycle_directed(n: int, edges: List[List[int]]) -> bool:
4 """Directed cycle detection with three colors (the check behind Course Schedule).
5 Time: O(V + E) | Space: O(V + E)
6 """
7 WHITE, GREY, BLACK = 0, 1, 2
8 state = [WHITE] * n
9 adj = [[] for _ in range(n)]
10 for u, v in edges:
11 adj[u].append(v)
12 
13 def dfs(u: int) -> bool:
14 state[u] = GREY # On the active path
15 for v in adj[u]:
16 if state[v] == GREY:
17 return True # Edge back into the active path: cycle
18 if state[v] == WHITE and dfs(v):
19 return True
20 state[u] = BLACK # All neighbours done: u can't reach a cycle
21 return False
22 
23 for node in range(n):
24 if state[node] == WHITE and dfs(node):
25 return True
26 return False
Invariant: The GREY nodes are exactly the nodes on the current DFS path, and every node reachable from a BLACK node is BLACK and on no cycle.

Give every node a state: WHITE = not visited, GREY = on the current DFS path, BLACK = finished. Reaching a GREY node from u means an edge back into the path that led to u, so that path and the edge form a cycle. A node turns BLACK only after all its neighbours return without finding one, so a BLACK node can't lead to a cycle and is never searched again. The trap is using one visited flag: reaching a BLACK node again through a second path is not a cycle, so GREY and BLACK must stay different.

See it run

Three states in a depth-first search, step by step

state [0, 0, 0, 0, 0, 0, 0]

state [1, 0, 0, 0, 0, 0, 0]

state [1, 1, 0, 0, 0, 0, 0]

state [1, 1, 1, 0, 0, 0, 0]

state [1, 1, 1, 0, 0, 1, 0]

state [1, 1, 1, 0, 0, 2, 0]

state [1, 1, 1, 0, 0, 2, 0]

state [1, 1, 2, 0, 0, 2, 0]

state [1, 1, 2, 0, 0, 2, 0]

state [1, 1, 2, 1, 0, 2, 0]

state [1, 1, 2, 1, 0, 2, 0]

state [1, 1, 2, 1, 0, 2, 0]

state [1, 1, 2, 1, 1, 2, 0]

state [1, 1, 2, 1, 1, 2, 0]

state [1, 1, 2, 1, 2, 2, 0]

state [1, 1, 2, 1, 2, 2, 1]

state [1, 1, 2, 1, 2, 2, 2]

state [1, 1, 2, 1, 2, 2, 2] · answer [2, 4, 5, 6]

Each circle is a node and each arrow is an edge. State 0 is not seen, state 1 is on the current path, state 2 is finished. A node in state 0 is drawn plain.

  • now the node whose edges are being read, or the edge being followed
  • path state 1: a node of the current depth-first path
  • done state 2: every neighbour returned without a cycle
  • back an edge into a node that is on the current path
  • unsafe a node on a path that ended in a cycle, left out of state 2

Every node starts in state 0 (not seen). The outer loop will call dfs on each node that is still in state 0.

The outer loop reaches node 0, which is not seen. Call dfs(0): state[0] becomes 1.

Edge 0 to 1: node 1 is not seen. Call dfs(1): state[1] becomes 1 and 1 joins the path.

Edge 1 to 2: node 2 is not seen. Call dfs(2): state[2] becomes 1 and 2 joins the path.

Edge 2 to 5: node 5 is not seen. Call dfs(5): state[5] becomes 1 and 5 joins the path.

Every neighbour of node 5 returned without a cycle, so state[5] becomes 2 (finished).

dfs(5) returned False, so the search is back at node 2. Its next neighbour is checked.

Every neighbour of node 2 returned without a cycle, so state[2] becomes 2 (finished).

dfs(2) returned False, so the search is back at node 1. Its next neighbour is checked.

Edge 1 to 3: node 3 is not seen. Call dfs(3): state[3] becomes 1 and 3 joins the path.

Edge 3 to 0: node 0 is on the current path. This edge goes back into the path, so it closes a cycle and dfs(3) returns True.

dfs returned True through nodes 0, 1, 3. These nodes stay in state 1 and never reach state 2, so they are unsafe.

The outer loop reaches node 4, which is not seen. Call dfs(4): state[4] becomes 1.

Edge 4 to 5: node 5 is finished (state 2). It cannot reach a cycle, so this is not a cycle. Skip it.

Every neighbour of node 4 returned without a cycle, so state[4] becomes 2 (finished).

The outer loop reaches node 6, which is not seen. Call dfs(6): state[6] becomes 1.

Every neighbour of node 6 returned without a cycle, so state[6] becomes 2 (finished).

The outer loop is done. The nodes in state 2 are the safe nodes: [2, 4, 5, 6].

0123456
path
empty
done
empty
1 / 18
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Reporting a cycle at any visited node, BLACK included

    Mistake
    if state[v] != WHITE:
    Fix
    if state[v] == GREY:

    What happens: A BLACK node is finished, and no cycle runs through it. A second path can still reach it, as in a diamond 0 -> 1 -> 3 and 0 -> 2 -> 3. A single visited flag treats that second visit as a cycle. Only a GREY node, one on the current path, closes a cycle.

    Example: n = 4, edges = [[0, 1], [0, 2], [1, 3], [2, 3]]
    Mistake: returns TrueFix: returns False
  2. 2

    Gotcha 2: Marking u as BLACK before its neighbours are searched

    Mistake
    state[u] = BLACK
    Fix
    state[u] = GREY

    What happens: u must stay GREY while the search is below it. A path that comes back to u then finds it GREY and reports the cycle. If u is BLACK from the start, no node is ever GREY. The search never reports a cycle.

    Example: n = 2, edges = [[0, 1], [1, 0]]
    Mistake: returns FalseFix: returns True
  3. 3

    Gotcha 3: Using the three colors on an undirected graph

    What happens: An undirected edge between u and v is stored in both directions. The search goes from u to v while u is GREY. From v, the edge back to u finds u GREY. So every edge looks like a cycle. An undirected graph needs a parent check or union-find instead.

  4. 4

    Gotcha 4: Keeping Python's default recursion limit for a 2000-node chain

    Mistake
    has_cycle_directed(n, edges)
    Fix
    sys.setrecursionlimit(10000)
    has_cycle_directed(n, edges)

    What happens: dfs calls itself once for each node on the current path. Course Schedule (LC 207) allows 2000 courses in one chain. That is 2000 nested calls, and Python's default limit is 1000. So the run raises a RecursionError. Raise the limit first, or keep the colors on an explicit stack.

    Example: n = 2000, edges = [[0, 1], [1, 2], ..., [1998, 1999]]
    Mistake: raises RecursionErrorFix: returns False
Not this one if…
  • You need the number of rounds when every task whose prerequisites are done runs at the same time, as in Parallel Courses (LC 1136), not just whether a cycle exists. See Topological Sort
  • Every node has exactly one next pointer and memory must stay O(1), as in Linked List Cycle (LC 141). See Fast & Slow Pointers

Deep Dive

Why a node on the current path closes a cycle

Each node has one of three states in state. WHITE (0) means not seen. GREY (1) means the node is on the current depth-first path, so dfs was called on it and has not returned. BLACK (2) means dfs has returned for it, and no cycle was found below it.

The nodes in state GREY are exactly the chain of calls that led to u: each one has an edge to the next one. Suppose u has an edge to a node v that is GREY. Then v is an ancestor of u on the path, and the path from v down to u plus the edge u -> v is a cycle.

A BLACK node is different. All of its neighbours returned without a cycle, so no cycle starts at it. Another path may reach it again, as in a diamond with the edges 0 -> 1, 0 -> 2, 1 -> 3 and 2 -> 3. The second visit to node 3 is not a cycle. So GREY and BLACK must be different states, and only GREY is a cycle.

Example: in Find Eventual Safe States (LC 802), a node is safe when every path from it ends at a node with no outgoing edge. With graph = [[1,2],[2,3],[5],[0],[5],[],[]], the edges 0 -> 1, 1 -> 3 and 3 -> 0 form a cycle. The safe nodes are [2, 4, 5, 6].

One trace: the path, the back edge and the safe nodes

The board replays the example above. For Find Eventual Safe States (LC 802), the search does not stop at the first cycle. It returns True up the path and goes on with the next node in the outer loop.

The search starts at node 0 and follows 0 -> 1, 1 -> 2 and 2 -> 5. Node 5 has no neighbour, so it becomes state 2. Then node 2 becomes state 2. Back at node 1, the edge 1 -> 3 leads to node 3, and node 3 has the edge 3 -> 0.

Node 0 is in state 1, because dfs(0) has not returned. The edge 3 -> 0 is the back edge, and dfs(3) returns True. The nodes 3, 1 and 0 stay in state 1 and never reach state 2. The board calls them unsafe.

The outer loop then starts dfs(4). The edge 4 -> 5 leads to a node in state 2, so it is not a cycle, and node 4 becomes state 2. Node 6 has no neighbour and becomes state 2. The nodes in state 2 are [2, 4, 5, 6], which is the LeetCode answer.

The trap: one visited flag is not enough

Suppose the test is if state[v] != WHITE, which treats every seen node as a cycle. In the trace, the edge 4 -> 5 reaches node 5, which is finished. The flag version reports a cycle there, and node 4 is marked unsafe. The result is [2, 5, 6] and not [2, 4, 5, 6].

The same fault appears in the diamond. Course Schedule (LC 207) with the prerequisites of a diamond has no cycle. The flag version says it has one. The correct test is if state[v] == GREY.

The order of the state changes matters too. u must become GREY before its neighbours are searched, and BLACK only after the loop over its neighbours. If u is set to BLACK at the start, no node is ever GREY, and [[0, 1], [1, 0]] is reported as having no cycle.

Do not use this on an undirected graph. An undirected edge is stored in both directions, so the search from v sees the edge back to its parent u, which is GREY. Every edge looks like a cycle. An undirected graph needs a parent check or Union-Find.

The cost, and the neighbouring cards

Each node is entered once, because dfs is called only on a WHITE node and turns it to GREY at once. Each edge is read once, when its first node is processed. The time is O(V + E), and the space is O(V + E) for adj and state, plus the recursion stack of up to V calls.

The recursion depth is the length of the longest path. Course Schedule (LC 207) allows 2000 courses in one chain, which is 2000 nested calls. Python's default limit is 1000, so raise it with sys.setrecursionlimit, or keep the three states on an explicit stack.

Peel Dependencies answers the same question about a cycle with a queue and in-degrees, and it also gives the order of the tasks, as in Course Schedule II (LC 210). Use it when the order or the number of rounds is asked, as in Parallel Courses (LC 1136). Use the three states when you need to know which nodes lead to a cycle, as in Find Eventual Safe States (LC 802).

When every node has exactly one next pointer and memory must stay O(1), as in Linked List Cycle (LC 141), Tortoise and Hare is the better tool.

60-Second Pitch

"For a cycle in a directed graph, I give each node one of three states: not seen, on the current path, or finished. dfs(u) marks u as on the path, then looks at each neighbour. A neighbour that is on the path means an edge back into the path, so there is a cycle. A neighbour that is not seen gets its own dfs. A finished neighbour is skipped, because it cannot reach a cycle. After all neighbours return, I mark u finished. A finished node is never searched again, so each node and edge is read once. The time is O(V + E). I need two different states for on the path and finished. With one visited flag, a diamond looks like a cycle. For safe nodes, the nodes that end finished are the answer."

Common Questions

Why do you need three states and not a simple visited set?

A visited set cannot tell a node on the current path from a node that is finished. Only an edge into the current path is a cycle. An edge into a finished node is fine, as in a diamond. The third state records that difference.

What are the time and space complexity?

O(V + E) time, because each node gets one dfs call and each edge is read once. O(V + E) space for the adjacency list and the states, plus a recursion stack that is as deep as the longest path, up to V calls.

How do you get the safe nodes in Find Eventual Safe States (LC 802)?

Run the search from every node that is not seen, and do not stop at the first cycle. When dfs returns True, leave the nodes of the path in state 1. At the end, the safe nodes are the ones in state 2, in increasing order. Another way is to reverse the edges and peel nodes whose neighbours are all safe, which is the Peel Dependencies idea.

Why does the check state[v] == GREY not work on an undirected graph?

An undirected edge is stored in both directions. When the search goes from u to v, the edge from v back to u finds u in state 1. That is the edge the search just came along, and it is not a cycle. A parent check or Union-Find is needed.

When would you use a queue and in-degrees instead?

When you need the order of the tasks or the number of rounds, as in Course Schedule II (LC 210) and Parallel Courses (LC 1136). The queue method also finds a cycle: if it cannot take every node, a cycle remains. It does not need recursion.

SWE Reasoning

Spot it

  • The graph is directed, and the task asks whether a cycle exists.
  • The wording says safe nodes, nodes that cannot reach a cycle, or "every path ends properly".
  • Dependencies or prerequisites are given as pairs, and the task asks whether all of them can be met.
  • You need to know which nodes lead to a cycle, and not only whether one exists.
  • Reaching a node twice is allowed when it is not on the current path, as in a diamond.

The invariant, step by step

  1. Every node starts in state 0, not seen. The outer loop calls dfs on each node that is still in state 0.
  2. dfs(u) sets state[u] to 1 first, so the nodes in state 1 are exactly the nodes on the current path.
  3. For each neighbour v of u: if state[v] is 1, the edge goes back into the path, so there is a cycle and dfs returns True.
  4. If state[v] is 0, dfs(v) is called. If that call returns True, the answer is passed up at once.
  5. If state[v] is 2, v is finished and cannot reach a cycle. The edge is skipped.
  6. After the loop over the neighbours, state[u] becomes 2. A node in state 2 has every neighbour in state 2, so no cycle is reachable from it.

Traps

  • One visited flag. Reaching a finished node again through a second path is not a cycle. Only state 1 is a cycle.
  • u set to state 2 before its neighbours are searched. No node is ever in state 1, so no cycle is found.
  • The three states on an undirected graph. Every edge looks like a cycle, because the edge back to the parent is found. Use a parent check or Union-Find.
  • Python's default recursion limit. A chain of 2000 nodes makes 2000 nested calls. Raise the limit or use an explicit stack.
  • Stopping at the first cycle when the question is about safe nodes. Keep going with the outer loop. The nodes that never reach state 2 are the unsafe ones.

Practice

Canonical LeetCode:Course Schedule (LC 207)Find Eventual Safe States (LC 802)All Paths from Source Lead to Destination (LC 1059)Largest Color Value in a Directed Graph (LC 1857)Course Schedule II (LC 210)Count Visited Nodes in a Directed Graph (LC 2876)
Theory Context•Graph Algorithms

Builds on Flood Fill, which you haven’t finished yet.

P2MediumBuilt on Flood Fill~6 min speedrun

Cycle Detection in a Directed Graph (DFS Three-Color)

Built on
Flood FillA node has three states, not seen, on the current path and finished, and reaching a node on the current path proves a cycle.
Grows into
Strongly Connected Components
Unlocks
Tarjan's Bridges
The problem

Course Schedule (LC 207)(opens leetcode.com)

You have numCourses courses to complete, numbered 0 through numCourses - 1, and a list prerequisites where each entry [ai, bi] means course bi must be completed before course ai.

  • So an entry [0, 1] means course 1 has to be finished before course 0.

Decide whether every course can be completed at all — return true if a valid completion order exists, and false otherwise.

Example 1
Input:numCourses = 2, prerequisites = [[1,0]]
Output:true
With 2 courses total, course 1's only requirement is course 0, so taking course 0 then course 1 completes both.

Cycle Detection

WHITE (0=unvisited) ➔ GREY (1=visiting) ➔ BLACK (2=processed)
Spot it when: You must detect a cycle in a directed graph, find the nodes that can't reach a cycle (safe nodes), or check that every path ends properly.
Cycle Detection
Python
1from typing import List
2 
3def has_cycle_directed(n: int, edges: List[List[int]]) -> bool:
4 """Directed cycle detection with three colors (the check behind Course Schedule).
5 Time: O(V + E) | Space: O(V + E)
6 """
7 WHITE, GREY, BLACK = 0, 1, 2
8 state = [WHITE] * n
9 adj = [[] for _ in range(n)]
10 for u, v in edges:
11 adj[u].append(v)
12 
13 def dfs(u: int) -> bool:
14 state[u] = GREY # On the active path
15 for v in adj[u]:
16 if state[v] == GREY:
17 return True # Edge back into the active path: cycle
18 if state[v] == WHITE and dfs(v):
19 return True
20 state[u] = BLACK # All neighbours done: u can't reach a cycle
21 return False
22 
23 for node in range(n):
24 if state[node] == WHITE and dfs(node):
25 return True
26 return False
Invariant: The GREY nodes are exactly the nodes on the current DFS path, and every node reachable from a BLACK node is BLACK and on no cycle.

Give every node a state: WHITE = not visited, GREY = on the current DFS path, BLACK = finished. Reaching a GREY node from u means an edge back into the path that led to u, so that path and the edge form a cycle. A node turns BLACK only after all its neighbours return without finding one, so a BLACK node can't lead to a cycle and is never searched again. The trap is using one visited flag: reaching a BLACK node again through a second path is not a cycle, so GREY and BLACK must stay different.

See it run

Three states in a depth-first search, step by step

state [0, 0, 0, 0, 0, 0, 0]

state [1, 0, 0, 0, 0, 0, 0]

state [1, 1, 0, 0, 0, 0, 0]

state [1, 1, 1, 0, 0, 0, 0]

state [1, 1, 1, 0, 0, 1, 0]

state [1, 1, 1, 0, 0, 2, 0]

state [1, 1, 1, 0, 0, 2, 0]

state [1, 1, 2, 0, 0, 2, 0]

state [1, 1, 2, 0, 0, 2, 0]

state [1, 1, 2, 1, 0, 2, 0]

state [1, 1, 2, 1, 0, 2, 0]

state [1, 1, 2, 1, 0, 2, 0]

state [1, 1, 2, 1, 1, 2, 0]

state [1, 1, 2, 1, 1, 2, 0]

state [1, 1, 2, 1, 2, 2, 0]

state [1, 1, 2, 1, 2, 2, 1]

state [1, 1, 2, 1, 2, 2, 2]

state [1, 1, 2, 1, 2, 2, 2] · answer [2, 4, 5, 6]

Each circle is a node and each arrow is an edge. State 0 is not seen, state 1 is on the current path, state 2 is finished. A node in state 0 is drawn plain.

  • now the node whose edges are being read, or the edge being followed
  • path state 1: a node of the current depth-first path
  • done state 2: every neighbour returned without a cycle
  • back an edge into a node that is on the current path
  • unsafe a node on a path that ended in a cycle, left out of state 2

Every node starts in state 0 (not seen). The outer loop will call dfs on each node that is still in state 0.

The outer loop reaches node 0, which is not seen. Call dfs(0): state[0] becomes 1.

Edge 0 to 1: node 1 is not seen. Call dfs(1): state[1] becomes 1 and 1 joins the path.

Edge 1 to 2: node 2 is not seen. Call dfs(2): state[2] becomes 1 and 2 joins the path.

Edge 2 to 5: node 5 is not seen. Call dfs(5): state[5] becomes 1 and 5 joins the path.

Every neighbour of node 5 returned without a cycle, so state[5] becomes 2 (finished).

dfs(5) returned False, so the search is back at node 2. Its next neighbour is checked.

Every neighbour of node 2 returned without a cycle, so state[2] becomes 2 (finished).

dfs(2) returned False, so the search is back at node 1. Its next neighbour is checked.

Edge 1 to 3: node 3 is not seen. Call dfs(3): state[3] becomes 1 and 3 joins the path.

Edge 3 to 0: node 0 is on the current path. This edge goes back into the path, so it closes a cycle and dfs(3) returns True.

dfs returned True through nodes 0, 1, 3. These nodes stay in state 1 and never reach state 2, so they are unsafe.

The outer loop reaches node 4, which is not seen. Call dfs(4): state[4] becomes 1.

Edge 4 to 5: node 5 is finished (state 2). It cannot reach a cycle, so this is not a cycle. Skip it.

Every neighbour of node 4 returned without a cycle, so state[4] becomes 2 (finished).

The outer loop reaches node 6, which is not seen. Call dfs(6): state[6] becomes 1.

Every neighbour of node 6 returned without a cycle, so state[6] becomes 2 (finished).

The outer loop is done. The nodes in state 2 are the safe nodes: [2, 4, 5, 6].

0123456
path
empty
done
empty
1 / 18
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Reporting a cycle at any visited node, BLACK included

    Mistake
    if state[v] != WHITE:
    Fix
    if state[v] == GREY:

    What happens: A BLACK node is finished, and no cycle runs through it. A second path can still reach it, as in a diamond 0 -> 1 -> 3 and 0 -> 2 -> 3. A single visited flag treats that second visit as a cycle. Only a GREY node, one on the current path, closes a cycle.

    Example: n = 4, edges = [[0, 1], [0, 2], [1, 3], [2, 3]]
    Mistake: returns TrueFix: returns False
  2. 2

    Gotcha 2: Marking u as BLACK before its neighbours are searched

    Mistake
    state[u] = BLACK
    Fix
    state[u] = GREY

    What happens: u must stay GREY while the search is below it. A path that comes back to u then finds it GREY and reports the cycle. If u is BLACK from the start, no node is ever GREY. The search never reports a cycle.

    Example: n = 2, edges = [[0, 1], [1, 0]]
    Mistake: returns FalseFix: returns True
  3. 3

    Gotcha 3: Using the three colors on an undirected graph

    What happens: An undirected edge between u and v is stored in both directions. The search goes from u to v while u is GREY. From v, the edge back to u finds u GREY. So every edge looks like a cycle. An undirected graph needs a parent check or union-find instead.

  4. 4

    Gotcha 4: Keeping Python's default recursion limit for a 2000-node chain

    Mistake
    has_cycle_directed(n, edges)
    Fix
    sys.setrecursionlimit(10000)
    has_cycle_directed(n, edges)

    What happens: dfs calls itself once for each node on the current path. Course Schedule (LC 207) allows 2000 courses in one chain. That is 2000 nested calls, and Python's default limit is 1000. So the run raises a RecursionError. Raise the limit first, or keep the colors on an explicit stack.

    Example: n = 2000, edges = [[0, 1], [1, 2], ..., [1998, 1999]]
    Mistake: raises RecursionErrorFix: returns False
Not this one if…
  • You need the number of rounds when every task whose prerequisites are done runs at the same time, as in Parallel Courses (LC 1136), not just whether a cycle exists. See Topological Sort
  • Every node has exactly one next pointer and memory must stay O(1), as in Linked List Cycle (LC 141). See Fast & Slow Pointers

Deep Dive

Why a node on the current path closes a cycle

Each node has one of three states in state. WHITE (0) means not seen. GREY (1) means the node is on the current depth-first path, so dfs was called on it and has not returned. BLACK (2) means dfs has returned for it, and no cycle was found below it.

The nodes in state GREY are exactly the chain of calls that led to u: each one has an edge to the next one. Suppose u has an edge to a node v that is GREY. Then v is an ancestor of u on the path, and the path from v down to u plus the edge u -> v is a cycle.

A BLACK node is different. All of its neighbours returned without a cycle, so no cycle starts at it. Another path may reach it again, as in a diamond with the edges 0 -> 1, 0 -> 2, 1 -> 3 and 2 -> 3. The second visit to node 3 is not a cycle. So GREY and BLACK must be different states, and only GREY is a cycle.

Example: in Find Eventual Safe States (LC 802), a node is safe when every path from it ends at a node with no outgoing edge. With graph = [[1,2],[2,3],[5],[0],[5],[],[]], the edges 0 -> 1, 1 -> 3 and 3 -> 0 form a cycle. The safe nodes are [2, 4, 5, 6].

One trace: the path, the back edge and the safe nodes

The board replays the example above. For Find Eventual Safe States (LC 802), the search does not stop at the first cycle. It returns True up the path and goes on with the next node in the outer loop.

The search starts at node 0 and follows 0 -> 1, 1 -> 2 and 2 -> 5. Node 5 has no neighbour, so it becomes state 2. Then node 2 becomes state 2. Back at node 1, the edge 1 -> 3 leads to node 3, and node 3 has the edge 3 -> 0.

Node 0 is in state 1, because dfs(0) has not returned. The edge 3 -> 0 is the back edge, and dfs(3) returns True. The nodes 3, 1 and 0 stay in state 1 and never reach state 2. The board calls them unsafe.

The outer loop then starts dfs(4). The edge 4 -> 5 leads to a node in state 2, so it is not a cycle, and node 4 becomes state 2. Node 6 has no neighbour and becomes state 2. The nodes in state 2 are [2, 4, 5, 6], which is the LeetCode answer.

The trap: one visited flag is not enough

Suppose the test is if state[v] != WHITE, which treats every seen node as a cycle. In the trace, the edge 4 -> 5 reaches node 5, which is finished. The flag version reports a cycle there, and node 4 is marked unsafe. The result is [2, 5, 6] and not [2, 4, 5, 6].

The same fault appears in the diamond. Course Schedule (LC 207) with the prerequisites of a diamond has no cycle. The flag version says it has one. The correct test is if state[v] == GREY.

The order of the state changes matters too. u must become GREY before its neighbours are searched, and BLACK only after the loop over its neighbours. If u is set to BLACK at the start, no node is ever GREY, and [[0, 1], [1, 0]] is reported as having no cycle.

Do not use this on an undirected graph. An undirected edge is stored in both directions, so the search from v sees the edge back to its parent u, which is GREY. Every edge looks like a cycle. An undirected graph needs a parent check or Union-Find.

The cost, and the neighbouring cards

Each node is entered once, because dfs is called only on a WHITE node and turns it to GREY at once. Each edge is read once, when its first node is processed. The time is O(V + E), and the space is O(V + E) for adj and state, plus the recursion stack of up to V calls.

The recursion depth is the length of the longest path. Course Schedule (LC 207) allows 2000 courses in one chain, which is 2000 nested calls. Python's default limit is 1000, so raise it with sys.setrecursionlimit, or keep the three states on an explicit stack.

Peel Dependencies answers the same question about a cycle with a queue and in-degrees, and it also gives the order of the tasks, as in Course Schedule II (LC 210). Use it when the order or the number of rounds is asked, as in Parallel Courses (LC 1136). Use the three states when you need to know which nodes lead to a cycle, as in Find Eventual Safe States (LC 802).

When every node has exactly one next pointer and memory must stay O(1), as in Linked List Cycle (LC 141), Tortoise and Hare is the better tool.

60-Second Pitch

"For a cycle in a directed graph, I give each node one of three states: not seen, on the current path, or finished. dfs(u) marks u as on the path, then looks at each neighbour. A neighbour that is on the path means an edge back into the path, so there is a cycle. A neighbour that is not seen gets its own dfs. A finished neighbour is skipped, because it cannot reach a cycle. After all neighbours return, I mark u finished. A finished node is never searched again, so each node and edge is read once. The time is O(V + E). I need two different states for on the path and finished. With one visited flag, a diamond looks like a cycle. For safe nodes, the nodes that end finished are the answer."

Common Questions

Why do you need three states and not a simple visited set?

A visited set cannot tell a node on the current path from a node that is finished. Only an edge into the current path is a cycle. An edge into a finished node is fine, as in a diamond. The third state records that difference.

What are the time and space complexity?

O(V + E) time, because each node gets one dfs call and each edge is read once. O(V + E) space for the adjacency list and the states, plus a recursion stack that is as deep as the longest path, up to V calls.

How do you get the safe nodes in Find Eventual Safe States (LC 802)?

Run the search from every node that is not seen, and do not stop at the first cycle. When dfs returns True, leave the nodes of the path in state 1. At the end, the safe nodes are the ones in state 2, in increasing order. Another way is to reverse the edges and peel nodes whose neighbours are all safe, which is the Peel Dependencies idea.

Why does the check state[v] == GREY not work on an undirected graph?

An undirected edge is stored in both directions. When the search goes from u to v, the edge from v back to u finds u in state 1. That is the edge the search just came along, and it is not a cycle. A parent check or Union-Find is needed.

When would you use a queue and in-degrees instead?

When you need the order of the tasks or the number of rounds, as in Course Schedule II (LC 210) and Parallel Courses (LC 1136). The queue method also finds a cycle: if it cannot take every node, a cycle remains. It does not need recursion.

SWE Reasoning

Spot it

  • The graph is directed, and the task asks whether a cycle exists.
  • The wording says safe nodes, nodes that cannot reach a cycle, or "every path ends properly".
  • Dependencies or prerequisites are given as pairs, and the task asks whether all of them can be met.
  • You need to know which nodes lead to a cycle, and not only whether one exists.
  • Reaching a node twice is allowed when it is not on the current path, as in a diamond.

The invariant, step by step

  1. Every node starts in state 0, not seen. The outer loop calls dfs on each node that is still in state 0.
  2. dfs(u) sets state[u] to 1 first, so the nodes in state 1 are exactly the nodes on the current path.
  3. For each neighbour v of u: if state[v] is 1, the edge goes back into the path, so there is a cycle and dfs returns True.
  4. If state[v] is 0, dfs(v) is called. If that call returns True, the answer is passed up at once.
  5. If state[v] is 2, v is finished and cannot reach a cycle. The edge is skipped.
  6. After the loop over the neighbours, state[u] becomes 2. A node in state 2 has every neighbour in state 2, so no cycle is reachable from it.

Traps

  • One visited flag. Reaching a finished node again through a second path is not a cycle. Only state 1 is a cycle.
  • u set to state 2 before its neighbours are searched. No node is ever in state 1, so no cycle is found.
  • The three states on an undirected graph. Every edge looks like a cycle, because the edge back to the parent is found. Use a parent check or Union-Find.
  • Python's default recursion limit. A chain of 2000 nodes makes 2000 nested calls. Raise the limit or use an explicit stack.
  • Stopping at the first cycle when the question is about safe nodes. Keep going with the outer loop. The nodes that never reach state 2 are the unsafe ones.

Practice

Canonical LeetCode:Course Schedule (LC 207)Find Eventual Safe States (LC 802)All Paths from Source Lead to Destination (LC 1059)Largest Color Value in a Directed Graph (LC 1857)Course Schedule II (LC 210)Count Visited Nodes in a Directed Graph (LC 2876)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•