2360. Longest Cycle in a Graph
Problem Description
You have a directed graph with n
nodes labeled from 0
to n - 1
. Each node can have at most one outgoing edge (meaning each node points to at most one other node).
The graph is given as an array edges
of size n
, where:
edges[i]
represents the node that nodei
points to- If node
i
has no outgoing edge, thenedges[i] = -1
Your task is to find the length of the longest cycle in this graph. A cycle is a path that starts at some node and eventually returns to that same starting node. If no cycle exists in the graph, return -1
.
For example:
- If
edges = [3, 3, 4, 2, 3]
, node 0 points to node 3, node 1 points to node 3, node 2 points to node 4, node 3 points to node 2, and node 4 points to node 3. There's a cycle: 2 β 4 β 3 β 2, which has length 3. - If
edges = [2, -1, 3, 1]
, node 0 points to 2, node 1 has no outgoing edge, node 2 points to 3, and node 3 points to 1. There's no cycle in this graph, so the answer would be -1.
The key insight is that since each node has at most one outgoing edge, if you start from any node and keep following edges, you'll either:
- Reach a node with no outgoing edge (dead end)
- Enter a cycle
- Reach a node that's already been visited (which could be part of a cycle or a path to a dead end)
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem explicitly states we have a directed graph with
n
nodes numbered from0
ton - 1
, where each node has at most one outgoing edge represented by theedges
array.
Is it a tree?
- No: A tree is acyclic and connected, but this problem specifically asks us to find cycles in the graph. The presence of cycles means it cannot be a tree.
Is the problem related to directed acyclic graphs?
- No: The problem is about finding cycles, which means the graph may contain cycles. It's not about directed acyclic graphs (DAGs).
Is the problem related to shortest paths?
- No: We're looking for the longest cycle, not shortest paths between nodes.
Does the problem involve connectivity?
- No: While we do traverse connected components, the main goal is to find and measure cycles, not to determine connectivity between nodes.
Does the problem have small constraints?
- Not necessarily: The constraints can be up to
n = 10^5
nodes, which isn't particularly small.
However, the key insight is that we need to traverse the graph starting from each unvisited node and follow the edges until we either:
- Hit a dead end (
edges[i] = -1
) - Find a cycle (revisit a node in our current path)
- Reach an already visited node from a previous traversal
This traversal pattern where we follow a single path deeply before backtracking is characteristic of DFS (Depth-First Search).
Conclusion: The flowchart analysis, combined with the nature of following single outgoing edges and detecting cycles, points us to use a DFS approach. The solution uses DFS to traverse from each unvisited starting point, tracking the path until a cycle is detected or the path ends.
Intuition
Since each node has at most one outgoing edge, following edges from any starting node creates a single path (no branching). This path will eventually either:
- Hit a dead end (node with no outgoing edge)
- Enter a cycle
The key insight is that if we keep track of nodes in our current path, we can detect when we revisit a node - that's when we've found a cycle. The cycle length is the number of nodes from where we first encountered the repeated node until we reached it again.
Consider what happens when we traverse from a node:
- We build a path by following edges:
node β edges[node] β edges[edges[node]] β ...
- We mark each visited node to avoid reprocessing
- If we reach a node we've already seen in our current path, we've found a cycle
- The cycle doesn't include all nodes in our path - only those from the repeated node onwards
For example, if our path is 1 β 2 β 3 β 4 β 2
, the cycle is 2 β 3 β 4 β 2
with length 3, not the entire path.
Why do we need to try every unvisited node as a starting point? Because the graph might have multiple disconnected components, and cycles could exist in any of them. A node might lead to a cycle even if it's not part of the cycle itself.
The algorithm naturally emerges:
- For each unvisited node, start a new path traversal
- Follow edges while recording the path
- If we revisit a node from our current path, calculate the cycle length
- Mark all traversed nodes as visited to avoid redundant work
- Track the maximum cycle length found
This approach ensures we find all cycles in the graph with just one pass through all nodes, making it efficient with O(n)
time complexity.
Learn more about Depth-First Search, Breadth-First Search, Graph and Topological Sort patterns.
Solution Approach
The implementation uses a straightforward traversal approach where we visit each node as a potential starting point for finding cycles.
Data Structures Used:
vis
: A boolean array of sizen
to track visited nodes, preventing redundant traversalscycle
: A list to store the current path being explored from each starting node
Algorithm Steps:
-
Initialize tracking variables:
- Create a visited array
vis
of sizen
, all set toFalse
- Initialize
ans = -1
to store the maximum cycle length
- Create a visited array
-
Iterate through all nodes as potential starting points:
for i in range(n): if vis[i]: continue # Skip already visited nodes
-
For each unvisited starting node, build a path:
- Start from node
i
and follow edges using variablej
- Build the path in
cycle
list while marking nodes as visited
j = i cycle = [] while j != -1 and not vis[j]: vis[j] = True cycle.append(j) j = edges[j] # Move to next node
- Start from node
-
Detect and measure cycles:
- After the loop,
j
will be either:-1
: Path ended at a dead node (no cycle)- A visited node: Either part of current path (cycle) or from previous traversal
- If
j != -1
, check if it's in the current path:
k = next((k for k in range(m) if cycle[k] == j), inf)
- If
j
is found incycle
at indexk
, then nodes from indexk
to the end form a cycle - Cycle length =
m - k
wherem = len(cycle)
- After the loop,
-
Update maximum cycle length:
ans = max(ans, m - k)
Example Walkthrough:
Consider edges = [3, 3, 4, 2, 3]
:
- Starting from node 0: Path is
0 β 3 β 2 β 4 β 3
. Node 3 is revisited.cycle = [0, 3, 2, 4]
,j = 3
- Find 3 in cycle at index 1
- Cycle length = 4 - 1 = 3 (the cycle is
3 β 2 β 4 β 3
)
The algorithm efficiently handles all cases:
- Multiple disconnected components
- Nodes leading to cycles without being part of them
- Dead-end paths
- Already processed nodes from previous traversals
Time complexity: O(n)
- each node is visited at most once
Space complexity: O(n)
- for the visited array and cycle list
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the algorithm with edges = [1, 2, 0, 4, 5, 3]
.
This graph looks like:
- 0 β 1 β 2 β 0 (forms a cycle of length 3)
- 3 β 4 β 5 β 3 (forms a cycle of length 3)
Step-by-step execution:
Iteration 1: Start from node 0
vis = [False, False, False, False, False, False]
- Build path: 0 β 1 β 2 β 0
cycle = [0, 1, 2]
, thenj = edges[2] = 0
- Node 0 is already visited (it's in our current path)
- Find 0 in cycle: it's at index 0
- Cycle length = 3 - 0 = 3
- Update
ans = 3
- Mark nodes visited:
vis = [True, True, True, False, False, False]
Iteration 2: Start from node 1
- Skip (already visited)
Iteration 3: Start from node 2
- Skip (already visited)
Iteration 4: Start from node 3
vis = [True, True, True, False, False, False]
- Build path: 3 β 4 β 5 β 3
cycle = [3, 4, 5]
, thenj = edges[5] = 3
- Node 3 is already visited (it's in our current path)
- Find 3 in cycle: it's at index 0
- Cycle length = 3 - 0 = 3
ans = max(3, 3) = 3
- Mark nodes visited:
vis = [True, True, True, True, True, True]
Iterations 5-6: Start from nodes 4 and 5
- Skip (already visited)
Result: Maximum cycle length = 3
The algorithm correctly identifies both cycles in the graph with just one pass through all nodes, demonstrating its O(n) efficiency.
Solution Implementation
1class Solution:
2 def longestCycle(self, edges: List[int]) -> int:
3 n = len(edges)
4 visited = [False] * n
5 max_cycle_length = -1
6
7 # Try starting from each unvisited node
8 for start_node in range(n):
9 if visited[start_node]:
10 continue
11
12 # Track the path from current starting node
13 current_node = start_node
14 path = []
15
16 # Follow edges until we hit a visited node or dead end
17 while current_node != -1 and not visited[current_node]:
18 visited[current_node] = True
19 path.append(current_node)
20 current_node = edges[current_node]
21
22 # If we hit a dead end (no outgoing edge), skip
23 if current_node == -1:
24 continue
25
26 # Check if the node we stopped at is part of current path
27 # If yes, we found a cycle
28 path_length = len(path)
29
30 # Find the index of current_node in path
31 # If current_node is in path, we have a cycle
32 cycle_start_index = float('inf')
33 for index in range(path_length):
34 if path[index] == current_node:
35 cycle_start_index = index
36 break
37
38 # Update max cycle length if we found a cycle
39 cycle_length = path_length - cycle_start_index
40 max_cycle_length = max(max_cycle_length, cycle_length)
41
42 return max_cycle_length
43
1class Solution {
2 public int longestCycle(int[] edges) {
3 int n = edges.length;
4 boolean[] visited = new boolean[n];
5 int longestCycleLength = -1;
6
7 // Try to find cycles starting from each unvisited node
8 for (int startNode = 0; startNode < n; startNode++) {
9 // Skip if this node has already been visited
10 if (visited[startNode]) {
11 continue;
12 }
13
14 // Track the path from current starting node
15 List<Integer> currentPath = new ArrayList<>();
16 int currentNode = startNode;
17
18 // Traverse the graph following edges until we hit a visited node or dead end
19 while (currentNode != -1 && !visited[currentNode]) {
20 visited[currentNode] = true;
21 currentPath.add(currentNode);
22 currentNode = edges[currentNode];
23 }
24
25 // If we reached a dead end (no outgoing edge), no cycle found
26 if (currentNode == -1) {
27 continue;
28 }
29
30 // Check if the node we stopped at is part of the current path (forms a cycle)
31 for (int pathIndex = 0; pathIndex < currentPath.size(); pathIndex++) {
32 if (currentPath.get(pathIndex) == currentNode) {
33 // Calculate cycle length from where the cycle starts to the end of path
34 int cycleLength = currentPath.size() - pathIndex;
35 longestCycleLength = Math.max(longestCycleLength, cycleLength);
36 break;
37 }
38 }
39 }
40
41 return longestCycleLength;
42 }
43}
44
1class Solution {
2public:
3 int longestCycle(vector<int>& edges) {
4 int n = edges.size();
5 vector<bool> visited(n, false);
6 int maxCycleLength = -1;
7
8 // Try to find cycles starting from each unvisited node
9 for (int startNode = 0; startNode < n; ++startNode) {
10 // Skip if this node has already been visited
11 if (visited[startNode]) {
12 continue;
13 }
14
15 // Traverse the path starting from current node
16 int currentNode = startNode;
17 vector<int> path;
18
19 // Follow edges until we reach a visited node or a dead end (-1)
20 while (currentNode != -1 && !visited[currentNode]) {
21 visited[currentNode] = true;
22 path.push_back(currentNode);
23 currentNode = edges[currentNode];
24 }
25
26 // If we reached a dead end, no cycle exists in this path
27 if (currentNode == -1) {
28 continue;
29 }
30
31 // Check if the visited node is part of the current path (forms a cycle)
32 for (int pathIndex = 0; pathIndex < path.size(); ++pathIndex) {
33 if (path[pathIndex] == currentNode) {
34 // Calculate cycle length from where the cycle starts
35 int cycleLength = static_cast<int>(path.size()) - pathIndex;
36 maxCycleLength = max(maxCycleLength, cycleLength);
37 break;
38 }
39 }
40 }
41
42 return maxCycleLength;
43 }
44};
45
1/**
2 * Finds the longest cycle in a directed graph represented by edges array
3 * @param edges - Array where edges[i] represents an edge from node i to node edges[i], or -1 if no outgoing edge
4 * @returns The length of the longest cycle, or -1 if no cycle exists
5 */
6function longestCycle(edges: number[]): number {
7 const nodeCount: number = edges.length;
8 const visited: boolean[] = Array(nodeCount).fill(false);
9 let maxCycleLength: number = -1;
10
11 // Try to find cycles starting from each unvisited node
12 for (let startNode = 0; startNode < nodeCount; ++startNode) {
13 // Skip if node already visited
14 if (visited[startNode]) {
15 continue;
16 }
17
18 // Traverse the path starting from current node
19 let currentNode: number = startNode;
20 const pathNodes: number[] = [];
21
22 // Follow edges until we hit a visited node or dead end (-1)
23 while (currentNode !== -1 && !visited[currentNode]) {
24 visited[currentNode] = true;
25 pathNodes.push(currentNode);
26 currentNode = edges[currentNode];
27 }
28
29 // If we hit a dead end, no cycle found in this path
30 if (currentNode === -1) {
31 continue;
32 }
33
34 // Check if the visited node is part of the current path (forms a cycle)
35 for (let pathIndex = 0; pathIndex < pathNodes.length; ++pathIndex) {
36 if (pathNodes[pathIndex] === currentNode) {
37 // Calculate cycle length from where cycle starts to end of path
38 const cycleLength: number = pathNodes.length - pathIndex;
39 maxCycleLength = Math.max(maxCycleLength, cycleLength);
40 break;
41 }
42 }
43 }
44
45 return maxCycleLength;
46}
47
Time and Space Complexity
Time Complexity: O(n)
The algorithm visits each node at most once throughout the entire execution. In the outer loop, we iterate through all n
nodes. For each unvisited node, we start a traversal that follows edges until we either hit a visited node or reach a dead end (-1
). Since each node is marked as visited when first encountered and never revisited, the total number of edge traversals across all iterations is bounded by n
. The inner operations, including the list comprehension to find the index of j
in the cycle, run in O(m)
where m
is the length of the current path, but since the sum of all path lengths cannot exceed n
, the overall time complexity remains O(n)
.
Space Complexity: O(n)
The algorithm uses additional space for:
- The
vis
array of sizen
to track visited nodes:O(n)
- The
cycle
list that stores nodes in the current path, which in the worst case can contain up ton
nodes:O(n)
- A few integer variables (
i
,j
,k
,m
,ans
):O(1)
Therefore, the total space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Incorrectly Identifying Cycles
The Problem: A common mistake is assuming that whenever we encounter a visited node, we've found a cycle. This is incorrect because the visited node might have been visited in a previous traversal from a different starting point, not in the current path.
Example of the Issue:
# INCORRECT approach
for start in range(n):
if visited[start]:
continue
current = start
length = 0
while current != -1 and not visited[current]:
visited[current] = True
length += 1
current = edges[current]
# WRONG: Assuming any visited node means a cycle
if current != -1: # current is visited
max_cycle = max(max_cycle, length) # This is wrong!
Consider edges = [1, 2, -1, 4, 5, 3]
:
- Starting from node 0:
0 β 1 β 2 β -1
(no cycle) - Starting from node 3:
3 β 4 β 5 β 3
(cycle of length 3) - But if we start from node 4 after processing node 0, we'd see
4 β 5 β 3
(already visited from previous traversal), and incorrectly count this as a cycle of length 3.
The Solution: Always check if the visited node is part of the CURRENT path, not just any visited node:
# CORRECT approach
path = []
while current != -1 and not visited[current]:
visited[current] = True
path.append(current)
current = edges[current]
# Check if current is in the current path
if current != -1 and current in path:
cycle_start = path.index(current)
cycle_length = len(path) - cycle_start
max_cycle = max(max_cycle, cycle_length)
Pitfall 2: Using Dictionary/Set for Path Instead of List
The Problem: Using a set or dictionary to track the current path loses the order information needed to calculate cycle length.
Example of the Issue:
# INCORRECT: Using set loses ordering
path_set = set()
while current != -1 and not visited[current]:
visited[current] = True
path_set.add(current)
current = edges[current]
if current in path_set:
# How do we calculate cycle length? We don't know the position!
cycle_length = len(path_set) # WRONG!
The Solution: Use a list to maintain order, or use a dictionary that maps nodes to their positions:
# Option 1: List (as shown in correct solution)
path = []
# ... traverse and append to path ...
if current in path:
cycle_length = len(path) - path.index(current)
# Option 2: Dictionary with positions
path_positions = {}
position = 0
while current != -1 and not visited[current]:
visited[current] = True
path_positions[current] = position
position += 1
current = edges[current]
if current in path_positions:
cycle_length = position - path_positions[current]
Pitfall 3: Not Handling the inf Return Value Properly
The Problem: Using next()
with a default of inf
without proper handling can cause issues in the max comparison.
Example of the Issue:
# Potential issue with inf
k = next((k for k in range(m) if cycle[k] == j), inf)
ans = max(ans, m - k) # If k is inf, m - k becomes negative infinity!
The Solution: Add a check before using the value:
# Better approach
cycle_start = -1
for k in range(len(path)):
if path[k] == current_node:
cycle_start = k
break
if cycle_start != -1: # Only update if cycle found
cycle_length = len(path) - cycle_start
max_cycle_length = max(max_cycle_length, cycle_length)
This ensures we only calculate and compare cycle lengths when an actual cycle is detected in the current path.
Which of the following array represent a max heap?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Want a Structured Path to Master System Design Too? Donβt Miss This!