Facebook Pixel

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 from 0 to n-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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. For each edge (u, v) in the graph, temporarily "remove" or ignore this edge
  2. Try to find the shortest path from u to v without using this edge
  3. 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, except dist[u] = 0
  • Use a queue starting with vertex u
  • For each vertex i popped from the queue, explore its neighbors j
  • 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 Evaluator

Example 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, requiring O(m) space (each edge is stored twice in the undirected graph representation)
  • The dist array in BFS which uses O(n) space for tracking distances to all vertices
  • The queue q in BFS which can contain at most O(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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What data structure does Breadth-first search typically uses to store intermediate states?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More