2608. Shortest Cycle in a Graph
Problem Description
You're given a bi-directional graph with n
vertices labeled from 0
to n - 1
. The graph is represented by a 2D integer array edges
, where each edges[i] = [ui, vi]
represents an undirected edge between vertex ui
and vertex vi
.
The graph has the following properties:
- Each pair of vertices is connected by at most one edge (no multiple edges between the same pair)
- No vertex has an edge to itself (no self-loops)
Your task is to find the length of the shortest cycle in the graph. A cycle is defined as a path that:
- Starts and ends at the same vertex
- Uses each edge in the path only once
If no cycle exists in the graph, return -1
.
For example, if you have edges [[0,1], [1,2], [2,0]]
, this forms a triangle cycle of length 3. The path would be: 0 β 1 β 2 β 0, using 3 edges.
The solution approach works by considering each edge (u, v)
in the graph. For each edge, it temporarily "removes" that edge and then finds the shortest path from u
to v
without using that edge. If such a path exists, then combining it with the original edge (u, v)
forms a cycle. The length of this cycle would be the shortest path length plus 1 (for the edge itself). By checking all edges this way, we can find the minimum cycle length in the graph.
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 involves a graph with vertices and edges. We have
n
vertices labeled from0
ton-1
and edges connecting them.
Is it a tree?
- No: A tree is acyclic by definition, but we're looking for cycles in this graph. The graph can have cycles, so it's not a tree.
Is the problem related to directed acyclic graphs?
- No: The graph is bi-directional (undirected), and we're specifically looking for cycles, so it's not a DAG.
Is the problem related to shortest paths?
- Yes: We need to find the shortest cycle, which is essentially finding the shortest path that forms a cycle. The solution approach confirms this by finding shortest paths between vertices after temporarily removing edges.
Is the graph weighted?
- No: The edges don't have weights. Each edge has a length of 1 (unweighted graph).
BFS
- Yes: Since we have an unweighted graph and need to find shortest paths, BFS is the appropriate choice.
Conclusion: The flowchart suggests using BFS (Breadth-First Search) for finding the shortest cycle in an unweighted graph. This aligns perfectly with the solution approach, which uses BFS to find the shortest path between vertices u
and v
after temporarily excluding the edge (u, v)
, effectively finding the shortest alternative path that, when combined with the original edge, forms a cycle.
Intuition
To find the shortest cycle in a graph, we need to think about what a cycle actually is. A cycle means we can start from a vertex, travel through some edges, and return to the starting point. The key insight is that if we have an edge (u, v)
, and there exists another path from u
to v
that doesn't use this edge, then we've found a cycle!
Think of it this way: imagine you're standing at vertex u
and you can directly walk to vertex v
using the edge between them. Now, if you can find an alternative route from u
to v
without using that direct edge, you've discovered a cycle. The cycle would be: take the alternative path from u
to v
, then use the direct edge from v
back to u
.
This leads us to a clever approach:
- For each edge
(u, v)
in the graph, temporarily "remove" or ignore this edge - Try to find the shortest path from
u
tov
without using this edge - If such a path exists, the cycle length = length of this alternative path + 1 (for the original edge)
Why does this work? Because we're essentially finding the shortest "detour" around each edge. The shortest cycle containing edge (u, v)
must consist of that edge plus the shortest path from u
to v
that doesn't use that edge.
Since we want the shortest cycle in the entire graph, we check all edges and take the minimum cycle length found. We use BFS for finding the shortest path because the graph is unweighted (each edge has length 1), and BFS naturally finds the shortest path in unweighted graphs by exploring vertices level by level.
If no alternative path exists for any edge (meaning the graph is a tree or forest), then there are no cycles, and we return -1
.
Learn more about Breadth-First Search and Graph patterns.
Solution Approach
The implementation follows the edge enumeration + BFS strategy outlined in the reference approach:
1. Build the adjacency list:
First, we construct an adjacency list g
using a defaultdict(set)
where g[u]
contains all vertices directly connected to vertex u
. Since the graph is bi-directional, for each edge [u, v]
, we add v
to g[u]
and u
to g[v]
.
g = defaultdict(set)
for u, v in edges:
g[u].add(v)
g[v].add(u)
2. BFS function to find shortest path:
The bfs(u, v)
function finds the shortest path from vertex u
to vertex v
while excluding the direct edge between them:
- Initialize a distance array
dist
with infinity for all vertices, exceptdist[u] = 0
- Use a queue starting with vertex
u
- For each vertex
i
popped from the queue, explore its neighborsj
- Skip the edge if it's the one we're excluding:
(i, j) != (u, v) and (j, i) != (u, v)
- If vertex
j
hasn't been visited (dist[j] == inf
), update its distance and add to queue - Return
dist[v] + 1
which represents the cycle length (alternative path + the original edge)
def bfs(u: int, v: int) -> int:
dist = [inf] * n
dist[u] = 0
q = deque([u])
while q:
i = q.popleft()
for j in g[i]:
if (i, j) != (u, v) and (j, i) != (u, v) and dist[j] == inf:
dist[j] = dist[i] + 1
q.append(j)
return dist[v] + 1
3. Find the minimum cycle: We enumerate all edges and compute the cycle length for each:
ans = min(bfs(u, v) for u, v in edges)
If no cycle exists (all BFS calls return infinity), we return -1
. Otherwise, we return the minimum cycle length found.
The time complexity is O(E Γ (V + E))
where E
is the number of edges and V
is the number of vertices, as we run BFS for each edge. The space complexity is O(V + E)
for storing the graph and the BFS queue.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with the graph having edges: [[0,1], [1,2], [2,3], [3,0], [0,2]]
This creates a graph that looks like:
0 --- 1 |\ | | \ | | \ | 3 --- 2
Step 1: Build the adjacency list
g[0] = {1, 2, 3} g[1] = {0, 2} g[2] = {0, 1, 3} g[3] = {0, 2}
Step 2: Check each edge for cycles
Edge (0,1):
- Remove edge (0,1) temporarily
- Run BFS from vertex 0 to vertex 1 without using this edge
- Path found: 0 β 2 β 1 (length = 2)
- Cycle length = 2 + 1 = 3
- The cycle is: 0 β 1 β 2 β 0
Edge (1,2):
- Remove edge (1,2) temporarily
- Run BFS from vertex 1 to vertex 2 without using this edge
- Path found: 1 β 0 β 2 (length = 2)
- Cycle length = 2 + 1 = 3
- The cycle is: 1 β 2 β 0 β 1
Edge (2,3):
- Remove edge (2,3) temporarily
- Run BFS from vertex 2 to vertex 3 without using this edge
- Path found: 2 β 0 β 3 (length = 2)
- Cycle length = 2 + 1 = 3
- The cycle is: 2 β 3 β 0 β 2
Edge (3,0):
- Remove edge (3,0) temporarily
- Run BFS from vertex 3 to vertex 0 without using this edge
- Path found: 3 β 2 β 0 (length = 2)
- Cycle length = 2 + 1 = 3
- The cycle is: 3 β 0 β 2 β 3
Edge (0,2):
- Remove edge (0,2) temporarily
- Run BFS from vertex 0 to vertex 2 without using this edge
- Path found: 0 β 1 β 2 (length = 2)
- Cycle length = 2 + 1 = 3
- The cycle is: 0 β 2 β 1 β 0
Step 3: Find minimum All edges give us cycle length of 3. The minimum cycle length is 3.
Note that there's also a 4-cycle (0 β 1 β 2 β 3 β 0), but our algorithm correctly identifies the shortest cycle of length 3.
Solution Implementation
1from collections import defaultdict, deque
2from typing import List
3
4class Solution:
5 def findShortestCycle(self, n: int, edges: List[List[int]]) -> int:
6 """
7 Find the length of the shortest cycle in an undirected graph.
8
9 Args:
10 n: Number of nodes in the graph (0 to n-1)
11 edges: List of edges where each edge is [u, v]
12
13 Returns:
14 Length of shortest cycle, or -1 if no cycle exists
15 """
16
17 def bfs_shortest_path(start_node: int, end_node: int) -> int:
18 """
19 Find shortest path from start_node to end_node excluding the direct edge.
20
21 Args:
22 start_node: Starting node for BFS
23 end_node: Target node to reach
24
25 Returns:
26 Distance from start to end plus 1 (to form cycle), or inf if unreachable
27 """
28 # Initialize distances to infinity
29 distances = [float('inf')] * n
30 distances[start_node] = 0
31
32 # BFS queue starting from start_node
33 queue = deque([start_node])
34
35 while queue:
36 current_node = queue.popleft()
37
38 # Explore all neighbors of current node
39 for neighbor in adjacency_list[current_node]:
40 # Skip the direct edge between start_node and end_node
41 # This ensures we find an alternate path to form a cycle
42 if ((current_node, neighbor) == (start_node, end_node) or
43 (current_node, neighbor) == (end_node, start_node)):
44 continue
45
46 # If neighbor hasn't been visited yet
47 if distances[neighbor] == float('inf'):
48 distances[neighbor] = distances[current_node] + 1
49 queue.append(neighbor)
50
51 # Return cycle length (path length + 1 for the direct edge)
52 return distances[end_node] + 1
53
54 # Build adjacency list representation of the graph
55 adjacency_list = defaultdict(set)
56 for u, v in edges:
57 adjacency_list[u].add(v)
58 adjacency_list[v].add(u)
59
60 # Try each edge as potential part of the shortest cycle
61 # For each edge, find shortest path between its endpoints without using that edge
62 min_cycle_length = min(bfs_shortest_path(u, v) for u, v in edges)
63
64 # Return result: cycle length if found, otherwise -1
65 return min_cycle_length if min_cycle_length < float('inf') else -1
66
1class Solution {
2 // Adjacency list representation of the graph
3 private List<Integer>[] adjacencyList;
4 // Constant representing infinity (unreachable distance)
5 private final int INFINITY = 1 << 30;
6
7 public int findShortestCycle(int n, int[][] edges) {
8 // Initialize the adjacency list for n vertices
9 adjacencyList = new List[n];
10 Arrays.setAll(adjacencyList, vertex -> new ArrayList<>());
11
12 // Build the undirected graph from the edges
13 for (int[] edge : edges) {
14 int vertex1 = edge[0];
15 int vertex2 = edge[1];
16 adjacencyList[vertex1].add(vertex2);
17 adjacencyList[vertex2].add(vertex1);
18 }
19
20 // Try removing each edge and find the shortest path between its endpoints
21 int shortestCycleLength = INFINITY;
22 for (int[] edge : edges) {
23 int startVertex = edge[0];
24 int endVertex = edge[1];
25 // Find shortest path from startVertex to endVertex without using this edge
26 // Adding 1 to account for the removed edge completes the cycle
27 shortestCycleLength = Math.min(shortestCycleLength, bfs(startVertex, endVertex));
28 }
29
30 // Return the shortest cycle length, or -1 if no cycle exists
31 return shortestCycleLength < INFINITY ? shortestCycleLength : -1;
32 }
33
34 private int bfs(int startVertex, int endVertex) {
35 // Distance array to track shortest distances from startVertex
36 int[] distance = new int[adjacencyList.length];
37 Arrays.fill(distance, INFINITY);
38 distance[startVertex] = 0;
39
40 // Queue for BFS traversal
41 Deque<Integer> queue = new ArrayDeque<>();
42 queue.offer(startVertex);
43
44 // Perform BFS to find shortest path from startVertex to endVertex
45 while (!queue.isEmpty()) {
46 int currentVertex = queue.poll();
47
48 // Explore all neighbors of the current vertex
49 for (int neighbor : adjacencyList[currentVertex]) {
50 // Skip the direct edge between startVertex and endVertex
51 // This edge is temporarily "removed" to find alternate paths
52 boolean isDirectEdge = (currentVertex == startVertex && neighbor == endVertex) ||
53 (currentVertex == endVertex && neighbor == startVertex);
54
55 // Skip if this is the removed edge or if neighbor was already visited
56 if (isDirectEdge || distance[neighbor] != INFINITY) {
57 continue;
58 }
59
60 // Update distance and add neighbor to queue
61 distance[neighbor] = distance[currentVertex] + 1;
62 queue.offer(neighbor);
63 }
64 }
65
66 // Return the cycle length (shortest path + the removed edge)
67 return distance[endVertex] + 1;
68 }
69}
70
1class Solution {
2public:
3 int findShortestCycle(int n, vector<vector<int>>& edges) {
4 // Build adjacency list representation of the graph
5 vector<vector<int>> adjacencyList(n);
6 for (auto& edge : edges) {
7 int nodeU = edge[0];
8 int nodeV = edge[1];
9 adjacencyList[nodeU].push_back(nodeV);
10 adjacencyList[nodeV].push_back(nodeU);
11 }
12
13 const int INF = 1 << 30; // Large value representing infinity
14
15 // Lambda function to find shortest path between two nodes while excluding their direct edge
16 // This helps find the shortest cycle containing the edge (u, v)
17 auto findShortestPathExcludingEdge = [&](int startNode, int endNode) -> int {
18 // Initialize distances array with infinity
19 int distances[n];
20 fill(distances, distances + n, INF);
21 distances[startNode] = 0;
22
23 // BFS to find shortest path
24 queue<int> bfsQueue;
25 bfsQueue.push(startNode);
26
27 while (!bfsQueue.empty()) {
28 int currentNode = bfsQueue.front();
29 bfsQueue.pop();
30
31 // Explore all neighbors of current node
32 for (int neighbor : adjacencyList[currentNode]) {
33 // Skip if this is the direct edge we want to exclude (u->v or v->u)
34 bool isExcludedEdge = (currentNode == startNode && neighbor == endNode) ||
35 (currentNode == endNode && neighbor == startNode);
36
37 // Skip if already visited (distance is not infinity)
38 if (isExcludedEdge || distances[neighbor] != INF) {
39 continue;
40 }
41
42 // Update distance and add to queue
43 distances[neighbor] = distances[currentNode] + 1;
44 bfsQueue.push(neighbor);
45 }
46 }
47
48 // Return cycle length: shortest path from u to v (excluding direct edge) + 1 (for the direct edge)
49 return distances[endNode] + 1;
50 };
51
52 // Try each edge as part of a potential shortest cycle
53 int shortestCycleLength = INF;
54 for (auto& edge : edges) {
55 int nodeU = edge[0];
56 int nodeV = edge[1];
57 // Find shortest cycle containing this edge
58 shortestCycleLength = min(shortestCycleLength, findShortestPathExcludingEdge(nodeU, nodeV));
59 }
60
61 // Return -1 if no cycle exists, otherwise return the shortest cycle length
62 return shortestCycleLength < INF ? shortestCycleLength : -1;
63 }
64};
65
1/**
2 * Finds the length of the shortest cycle in an undirected graph
3 * @param n - Number of nodes in the graph (0 to n-1)
4 * @param edges - Array of edges where each edge is [u, v]
5 * @returns Length of shortest cycle, or -1 if no cycle exists
6 */
7function findShortestCycle(n: number, edges: number[][]): number {
8 // Build adjacency list representation of the graph
9 const adjacencyList: number[][] = new Array(n).fill(0).map(() => []);
10 for (const [u, v] of edges) {
11 adjacencyList[u].push(v);
12 adjacencyList[v].push(u);
13 }
14
15 // Initialize infinity value and minimum answer
16 const INFINITY = 1 << 30;
17 let shortestCycleLength = INFINITY;
18
19 /**
20 * BFS to find shortest path from startNode to endNode
21 * while excluding the direct edge between them
22 * @param startNode - Starting node for BFS
23 * @param endNode - Target node to reach
24 * @returns Length of cycle through this edge
25 */
26 const findCycleLengthThroughEdge = (startNode: number, endNode: number): number => {
27 // Initialize distances array with infinity
28 const distances: number[] = new Array(n).fill(INFINITY);
29 distances[startNode] = 0;
30
31 // BFS queue
32 const queue: number[] = [startNode];
33
34 while (queue.length > 0) {
35 const currentNode = queue.shift()!;
36
37 // Explore all neighbors
38 for (const neighbor of adjacencyList[currentNode]) {
39 // Skip the direct edge between startNode and endNode
40 if ((currentNode === startNode && neighbor === endNode) ||
41 (currentNode === endNode && neighbor === startNode) ||
42 distances[neighbor] !== INFINITY) {
43 continue;
44 }
45
46 // Update distance and add to queue
47 distances[neighbor] = distances[currentNode] + 1;
48 queue.push(neighbor);
49 }
50 }
51
52 // Return cycle length: shortest path + 1 (for the excluded edge)
53 return 1 + distances[endNode];
54 };
55
56 // Try removing each edge and find shortest path between its endpoints
57 for (const [u, v] of edges) {
58 shortestCycleLength = Math.min(shortestCycleLength, findCycleLengthThroughEdge(u, v));
59 }
60
61 // Return result: shortest cycle length or -1 if no cycle found
62 return shortestCycleLength < INFINITY ? shortestCycleLength : -1;
63}
64
Time and Space Complexity
Time Complexity: O(mΒ²)
where m
is the number of edges.
The algorithm iterates through each edge in the graph (total of m
edges) and performs a BFS for each edge. Each BFS operation visits at most n
vertices and traverses at most m
edges in the worst case, taking O(n + m)
time. Since we're dealing with a graph where cycles exist, and in the worst case of a dense graph, m
can be up to O(nΒ²)
. However, for each edge (u, v)
, the BFS explores the graph while excluding that specific edge, effectively searching for the shortest alternative path between u
and v
. This gives us a total time complexity of O(m Γ (n + m))
. In the worst case where the graph is dense (m β nΒ²
), this becomes O(mΒ²)
.
Space Complexity: O(m + n)
where n
is the number of vertices and m
is the number of edges.
The space is used for:
- The adjacency list
g
which stores all edges, requiringO(m)
space (each edge is stored twice in the undirected graph representation) - The
dist
array in BFS which usesO(n)
space for tracking distances to all vertices - The queue
q
in BFS which can contain at mostO(n)
vertices - The overall space complexity is therefore
O(m + n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrectly handling the edge exclusion in BFS
One of the most common mistakes is failing to properly exclude the direct edge between the two endpoints when searching for an alternative path. The edge exclusion check must be bidirectional since the graph is undirected.
Incorrect Implementation:
# Wrong: Only checking one direction if (current_node, neighbor) == (start_node, end_node): continue
Correct Implementation:
# Correct: Checking both directions of the edge if ((current_node, neighbor) == (start_node, end_node) or (current_node, neighbor) == (end_node, start_node)): continue
2. Using a list instead of a set for adjacency list values
Using a list for storing neighbors can lead to duplicate entries if the input contains duplicate edges, which would cause incorrect BFS traversal and potentially infinite loops.
Problematic Code:
# Using list can cause duplicates
adjacency_list = defaultdict(list)
for u, v in edges:
adjacency_list[u].append(v)
adjacency_list[v].append(u)
Better Solution:
# Using set automatically handles duplicates
adjacency_list = defaultdict(set)
for u, v in edges:
adjacency_list[u].add(v)
adjacency_list[v].add(u)
3. Forgetting to add 1 to the BFS distance
The BFS finds the shortest alternative path between two nodes, but the cycle length includes the original edge that was excluded. Forgetting to add 1 will give an incorrect cycle length.
Wrong:
return distances[end_node] # Missing the +1
Correct:
return distances[end_node] + 1 # Includes the direct edge
4. Not handling disconnected components properly
If the graph has disconnected components or isolated nodes, the solution handles it correctly by returning inf
when no path exists. However, a common mistake is to not check if the minimum value is still infinity before returning the result.
Potential Issue:
# If all edges are in disconnected components with no cycles
min_cycle_length = min(bfs_shortest_path(u, v) for u, v in edges)
return min_cycle_length # Could return inf instead of -1
Proper Handling:
min_cycle_length = min(bfs_shortest_path(u, v) for u, v in edges)
return min_cycle_length if min_cycle_length < float('inf') else -1
5. Performance degradation with dense graphs
While the algorithm is correct, for very dense graphs (where E approaches VΒ²), running BFS for each edge becomes expensive. Consider early termination optimizations:
Optimization Example:
def bfs_shortest_path(start_node: int, end_node: int, current_min: int) -> int:
# ... existing code ...
while queue:
current_node = queue.popleft()
# Early termination if current path is already longer than known minimum
if distances[current_node] >= current_min:
continue
for neighbor in adjacency_list[current_node]:
# ... rest of the logic
This optimization can significantly reduce computation time by pruning paths that cannot lead to a shorter cycle.
What data structure does Breadth-first search typically uses to store intermediate states?
Recommended Readings
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
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donβt Miss This!