2608. Shortest Cycle in a Graph
Problem Description
In this problem, we're given a bi-directional graph with n
vertices, labeled from 0
to n - 1
. The edges between these vertices are provided in the form of a 2D integer array edges
, where each sub-array has two integers representing an edge between the vertices it contains. The graph described does not allow for multiple edges between the same pair of vertices and does not allow for an edge to connect a vertex to itself.
We are asked to return the length of the shortest cycle within the graph. In graph theory, a cycle is a path that begins and ends at the same vertex and traverses each edge only once. If no such cycle exists, we should return -1
. This is a classic graph search problem that could involve techniques such as depth-first search (DFS) or breadth-first search (BFS).
Flowchart Walkthrough
Let's analyze the problem using the algorithm flowchart to determine the best approach. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem explicitly concerns cycles within a graph.
Is it a tree?
- No: Since the issue is to find the shortest cycle in a graph, and trees do not contain cycles, this graph cannot be a tree.
Is the problem related to directed acyclic graphs (DAGs)?
- No: We are dealing specifically with cycles in a graph, and a DAG by definition does not contain any cycles.
Is the problem related to shortest paths?
- Yes: The problem involves finding the shortest loop, which can be conceptualized as finding the shortest path that forms a cycle in the graph.
Is the graph weighted?
- Not specified explicitly but to use BFS efficiently, let's consider the graph unweighted for typical applications unless specified otherwise. Here, since the objective is to find the shortest cycle, standard BFS can be utilized efficiently in an unweighted graph because paths discovered first in BFS are the shortest.
Conclusion: Following the flowchart, BFS is a suitable approach for finding the shortest cycle in an unweighted graph. BFS can explore each node’s connections level-by-level, allowing it to efficiently determine the shortest cycle starting from each node.
Intuition
To find the shortest cycle, we need a way to traverse the graph and keep track of the distances from a starting node. The natural choice here is BFS, which allows us to explore the graph level by level, starting at a particular vertex.
We use BFS starting at each vertex and find the shortest path that returns to the starting vertex, thus creating a cycle. By maintaining an array of distances, we can check for the existence of a cycle whenever we visit a vertex that we have seen before, provided we didn't reach it by going back the way we came from (i.e., not backtracking).
For every vertex, we perform BFS and keep track of:
- The distance from the starting vertex to every other vertex we visit.
- The predicessor of each visited vertex to ensure we do not consider backtracking as part of a cycle.
When we discover a vertex that has been visited already (and is not the predecessor), we've found a cycle. We can calculate the cycle's length by summing up the distances (from the starting point to the current vertex and from the starting point to the visited vertex) and add 1 (for the edge that closes the cycle).
We repeat this process for each vertex in the graph and keep track of the minimum cycle length found. If we find no cycles at all after performing BFS from every vertex, we return -1
.
The provided solution leverages the BFS method combined with the idea of checking for cycles without backtracking to ensure the shortest path is calculated.
Learn more about Breadth-First Search and Graph patterns.
Solution Approach
To implement the solution, we're using two main approaches, each making use of Breadth-First Search (BFS) and adjacency lists to represent the graph. An adjacency list g
is a dictionary where each vertex key maps to a list of adjacent vertices, making it efficient to iterate over neighbours.
Solution 1: Enumerate edges + BFS
This approach constructs the adjacency list g
for representing the graph using the edges
array. With this adjacency graph, we can perform BFS from each vertex, looking for the shortest path to any other vertex.
For each vertex u
, we launch a BFS that computes the shortest distance to other vertices. During this search, if we find a vertex v
that we've seen before, and it's not the vertex we came from (not the parent, labeled fa
in the code), then we've discovered a cycle. We calculate the cycle's length as dist[u] + dist[v] + 1
, taking into account the distance of both vertices from the starting point and adding one for the edge that completes the cycle.
We execute this process for all possible edges, looking for the shortest cycle. If we find cycles, we return the length of the shortest one; otherwise, we return -1
.
Solution 2: Enumerate points + BFS
In the second approach, similar to the first, we also build the adjacency list g
. However, instead of enumerating the edges, we visit every vertex u
and perform BFS to look for cycles initiated from that vertex.
During the BFS, for each visited vertex v
, we check if there is another path to v
that doesn't go through the immediate parent (not backtracking). If such a path exists, we've found a cycle. The length of the cycle is then given by adding the distances of the two intersecting paths to v
, indicating the cycle's perimeter.
If, by going through all vertices, at least one cycle is found, we return the length of the shortest cycle found. Otherwise, our return value is -1
.
Data Structures and Patterns:
bfs(u: int) -> int
: A helper function for BFS, which takes a starting vertexu
and returns the length of the shortest cycle found when starting fromu
.defaultdict(list)
: To dynamically create the adjacency list without checking whether a vertex key already exists.deque
: A double-ended queue from thecollections
module used to efficiently pop vertices from the front of the queue while performing BFS.dist = [-1] * n
: An array that keeps track of distances to vertices from the starting vertex; initialized to-1
for vertices that haven't been visited.inf
: To initialize the minimum cycle length to infinity, so any cycle found will update this to a smaller value as cycles are detected.
Overall, the algorithms and data structures chosen here are tailored for efficiency and clarity when finding the shortest cycle in an undirected graph. The time complexity for Solution 1 is O(m^2)
and Solution 2 is also O(m^2)
(where m
is the number of edges, and n
is the number of vertices), because they both involve running BFS, which in the worst case examines all vertices and edges. The space complexity is O(m + n)
to accommodate the adjacency list and additional data structures used in the algorithms.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's consider a small bi-directional graph with 5 vertices (0 through 4) and 6 edges. The edges
array is given as follows:
edges = [[0, 1], [0, 2], [1, 2], [1, 3], [2, 3], [3, 4]]
Step 1: Build the Adjacency List
We would build an adjacency list from the given edges:
g = { 0: [1, 2], 1: [0, 2, 3], 2: [0, 1, 3], 3: [1, 2, 4], 4: [3] }
Step 2: Initialize Variables and Data Structures
Before we start BFS, we define:
dist
array to keep track of distances ([-1] * n
wheren = 5
).minCycle
variable to keep track of the shortest cycle found, initialized withinf
.
Step 3: BFS from Each Vertex
Perform BFS from each vertex. Let's illustrate a BFS starting at vertex 0
.
We enqueue the starting vertex (0, -1)
(vertex, parent) to a queue. Here, -1
means that the starting vertex has no parent.
BFS Iteration 1:
- We dequeue
(0, -1)
and markdist[0] = 0
. - We enqueue its neighbors
(1, 0)
and(2, 0)
with distancesdist[1] = 1
anddist[2] = 1
.
BFS Iteration 2:
- Dequeue
(1, 0)
. Enqueue neighbors(2, 1)
and(3, 1)
with distancesdist[2] = 2
anddist[3] = 2
. - As we visit node 2, we find that
2
is already visited and2
is not the parent of1
. Thus, we have found a cycle with nodes (0, 1, 2). The length of this cycle isdist[0] + dist[2] + 1 = 0 + 2 + 1 = 3
.
BFS from Other Vertices:
We repeat the above process starting from each of the other vertices (1
, 2
, 3
, 4
). Let's say we find no shorter cycle than length 3. Then 3 would be the answer.
Step 4: Return Shortest Cycle Length
If we find cycles, return the length of the shortest cycle found; otherwise, return -1
. In this case, we found a cycle of length 3
, so the function returns 3
.
This illustrative example uses the same steps outlined earlier in the Solution Approach
section, implementing the BFS algorithm to search for the shortest cycle by traversing the graph from each vertex. The dist
array helps ensure we are finding the shortest non-backtracking path, and the minCycle
variable updates with the smallest cycle length as we proceed.
Solution Implementation
1from collections import defaultdict, deque
2from math import inf
3from typing import List
4
5class Solution:
6 def findShortestCycle(self, num_nodes: int, edges: List[List[int]]) -> int:
7 # Local function to perform breadth-first search (BFS) starting from a node.
8 def bfs(start_node: int) -> int:
9 distances = [-1] * num_nodes
10 distances[start_node] = 0
11 queue = deque([(start_node, -1)])
12 shortest_cycle = inf
13
14 while queue:
15 current_node, parent_node = queue.popleft()
16
17 # Check all adjacent nodes to the current node.
18 for adjacent in graph[current_node]:
19 # If the adjacent node has not been visited.
20 if distances[adjacent] == -1:
21 distances[adjacent] = distances[current_node] + 1
22 queue.append((adjacent, current_node))
23 # If the adjacent node has been visited and is not the parent,
24 # we found a cycle.
25 elif adjacent != parent_node:
26 shortest_cycle = min(shortest_cycle, distances[current_node] + distances[adjacent] + 1)
27
28 return shortest_cycle
29
30 # Construct the graph as an adjacency list.
31 graph = defaultdict(list)
32 for u, v in edges:
33 graph[u].append(v)
34 graph[v].append(u)
35
36 # Apply BFS from each node to find the shortest cycle in the graph.
37 shortest_cycle_overall = min(bfs(i) for i in range(num_nodes))
38
39 # Return the length of the shortest cycle, or -1 if there is no cycle.
40 return shortest_cycle_overall if shortest_cycle_overall < inf else -1
41
1class Solution {
2 private List<Integer>[] graph;
3 private final int INFINITY = 1 << 30; // Represents a large number as infinity.
4
5 // Finds the shortest cycle in an undirected graph.
6 public int findShortestCycle(int n, int[][] edges) {
7 // Initialize the adjacency list for the graph.
8 graph = new List[n];
9 Arrays.setAll(graph, k -> new ArrayList<>());
10 // Populate the graph with the provided edges.
11 for (int[] edge : edges) {
12 int startVertex = edge[0], endVertex = edge[1];
13 graph[startVertex].add(endVertex);
14 graph[endVertex].add(startVertex);
15 }
16
17 int shortestCycleLength = INFINITY;
18 // Check each vertex as a starting point to find the shortest cycle.
19 for (int i = 0; i < n; ++i) {
20 shortestCycleLength = Math.min(shortestCycleLength, bfs(i));
21 }
22 // Return the length of the shortest cycle, or -1 if it doesn't exist.
23 return shortestCycleLength < INFINITY ? shortestCycleLength : -1;
24 }
25
26 // Conducts a breadth-first search to find the shortest path back to the starting node.
27 private int bfs(int startVertex) {
28 int[] distances = new int[graph.length];
29 Arrays.fill(distances, -1); // -1 signifies that a vertex has not been visited.
30 distances[startVertex] = 0; // The starting vertex has distance 0 from itself.
31 Deque<int[]> queue = new ArrayDeque<>();
32 queue.offer(new int[]{startVertex, -1}); // The second element in the array is the 'parent' vertex.
33
34 int shortestCycle = INFINITY;
35 while (!queue.isEmpty()) {
36 int[] currentPair = queue.poll();
37 int currentVertex = currentPair[0];
38 int parentVertex = currentPair[1];
39
40 for (int neighbor : graph[currentVertex]) {
41 if (distances[neighbor] < 0) {
42 // If the neighbor has not been visited, set its distance and add it to the queue.
43 distances[neighbor] = distances[currentVertex] + 1;
44 queue.offer(new int[] {neighbor, currentVertex});
45 } else if (neighbor != parentVertex) {
46 // If the neighbor has been visited and is not the parent, a cycle is found.
47 shortestCycle = Math.min(shortestCycle, distances[currentVertex] + distances[neighbor] + 1);
48 }
49 }
50 }
51 return shortestCycle;
52 }
53}
54
1#include <vector>
2#include <queue>
3#include <cstring>
4using namespace std;
5
6class Solution {
7public:
8 int findShortestCycle(int n, vector<vector<int>>& edges) {
9 // Graph representation using adjacency lists.
10 vector<vector<int>> graph(n);
11 for (auto& edge : edges) {
12 int u = edge[0], v = edge[1];
13 graph[u].push_back(v);
14 graph[v].push_back(u);
15 }
16
17 // Initialize the distance to be a very large number.
18 const int INF = 1 << 30;
19
20 // Lambda function to perform Breadth-First Search (BFS).
21 auto bfs = [&](int start) -> int {
22 vector<int> dist(n, -1); // Initialize distances to -1.
23 dist[start] = 0; // Distance to the start node is 0.
24 queue<pair<int, int>> q; // Queue to manage BFS.
25 q.emplace(start, -1); // Push start node with no parent (-1).
26
27 int shortestCycle = INF; // Store the shortest cycle length.
28 while (!q.empty()) {
29 auto [current, parent] = q.front();
30 q.pop();
31
32 for (int neighbor : graph[current]) {
33 if (dist[neighbor] < 0) {
34 // Neighbor not visited, set distance and enqueue.
35 dist[neighbor] = dist[current] + 1;
36 q.emplace(neighbor, current);
37 } else if (neighbor != parent) {
38 // Found a cycle, not through parent node.
39 shortestCycle = min(shortestCycle, dist[current] + dist[neighbor] + 1);
40 }
41 }
42 }
43 return shortestCycle;
44 };
45
46 // Search for the shortest cycle starting from each node.
47 int shortestCycleOverall = INF;
48 for (int i = 0; i < n; ++i) {
49 shortestCycleOverall = min(shortestCycleOverall, bfs(i));
50 }
51
52 // If a cycle was found, return its length; otherwise return -1.
53 return shortestCycleOverall < INF ? shortestCycleOverall : -1;
54 }
55};
56
1// Function to find the shortest cycle in an undirected graph.
2function findShortestCycle(n: number, edges: number[][]): number {
3 // Initialize graph as adjacency list.
4 const graph: number[][] = new Array(n).fill(0).map(() => []);
5
6 // Populate graph with edges.
7 for (const [from, to] of edges) {
8 graph[from].push(to);
9 graph[to].push(from);
10 }
11
12 // Define infinity as a value much larger than any possible path.
13 const infinity = 1 << 30;
14
15 // Temporary variable to hold the shortest cycle length found.
16 let shortestCycleLength = infinity;
17
18 // Helper function to perform breadth-first search (BFS) from a starting node.
19 const breadthFirstSearch = (startNode: number): number => {
20 // Initialize distances array with -1, signifying unvisited nodes.
21 const distances: number[] = new Array(n).fill(-1);
22
23 // The distance to the start node is 0.
24 distances[startNode] = 0;
25
26 // Queue for BFS. Each element is a tuple [node, parent].
27 const queue: number[][] = [[startNode, -1]];
28
29 // Initialize the shortest cycle length found in this BFS.
30 let cycleLength = infinity;
31
32 // Process nodes in the queue.
33 while (queue.length) {
34 const [currentNode, parentNode] = queue.shift()!;
35
36 // Check all adjacent nodes.
37 for (const neighbor of graph[currentNode]) {
38 // If the neighbor is unvisited, update its distance and add to queue.
39 if (distances[neighbor] < 0) {
40 distances[neighbor] = distances[currentNode] + 1;
41 queue.push([neighbor, currentNode]);
42 } else if (neighbor !== parentNode) {
43 // If the neighbor has been visited and isn't the parent,
44 // a cycle has been detected, update the shortest cycle length.
45 cycleLength = Math.min(cycleLength, distances[currentNode] + distances[neighbor] + 1);
46 }
47 }
48 }
49 return cycleLength;
50 };
51
52 // Iterate over all nodes to conduct BFS and update the shortest cycle length.
53 for (let i = 0; i < n; ++i) {
54 shortestCycleLength = Math.min(shortestCycleLength, breadthFirstSearch(i));
55 }
56
57 // If a cycle has been detected, return its length; otherwise, return -1.
58 return shortestCycleLength < infinity ? shortestCycleLength : -1;
59}
60
Time and Space Complexity
The provided code defines a class Solution
with a method findShortestCycle
that computes the shortest cycle in an undirected graph represented by its number of vertices n
and edges edges
.
Time Complexity
The time complexity of the method findShortestCycle
primarily depends on the breadth-first search (BFS) operation performed inside the bfs
local function.
For each vertex i
in the range 0
to n-1
, we perform a BFS:
- In the worst case, the BFS from a single vertex visits all
n
vertices and considers allm
edges in the graph. This happens because we have to check each edge to find any possible cycles, which translates to a complexity ofO(m)
. - Since we perform this BFS for each of the
n
vertices, the overall time complexity isO(n * m)
.
Thus, the total time complexity of findShortestCycle
is O(n * m)
.
Space Complexity
The space complexity consists of the storage used by:
- The adjacency list
g
, which contains an entry for each vertex. Each entry has a list of adjacent vertices. In the worst case, if the graph is fully connected, this would takeO(n + m)
space, as each edge contributes to two vertex lists. - The
dist
array, which is of lengthn
and stores distances from the start vertex for BFS. - The BFS queue
q
, which in the worst case can contain all vertices, inducing anO(n)
space requirement.
Adding these contributions together, the space complexity is O(m + n)
for the adjacency list and O(n)
for both dist
array and the queue, leading to a total space complexity being O(n + m)
.
To summarize, the space complexity of the findShortestCycle
method is O(m + n)
.
Learn more about how to find time and space complexity quickly using problem constraints.
What's the output of running the following function using input [30, 20, 10, 100, 33, 12]
?
1def fun(arr: List[int]) -> List[int]:
2 import heapq
3 heapq.heapify(arr)
4 res = []
5 for i in range(3):
6 res.append(heapq.heappop(arr))
7 return res
8
1public static int[] fun(int[] arr) {
2 int[] res = new int[3];
3 PriorityQueue<Integer> heap = new PriorityQueue<>();
4 for (int i = 0; i < arr.length; i++) {
5 heap.add(arr[i]);
6 }
7 for (int i = 0; i < 3; i++) {
8 res[i] = heap.poll();
9 }
10 return res;
11}
12
1class HeapItem {
2 constructor(item, priority = item) {
3 this.item = item;
4 this.priority = priority;
5 }
6}
7
8class MinHeap {
9 constructor() {
10 this.heap = [];
11 }
12
13 push(node) {
14 // insert the new node at the end of the heap array
15 this.heap.push(node);
16 // find the correct position for the new node
17 this.bubble_up();
18 }
19
20 bubble_up() {
21 let index = this.heap.length - 1;
22
23 while (index > 0) {
24 const element = this.heap[index];
25 const parentIndex = Math.floor((index - 1) / 2);
26 const parent = this.heap[parentIndex];
27
28 if (parent.priority <= element.priority) break;
29 // if the parent is bigger than the child then swap the parent and child
30 this.heap[index] = parent;
31 this.heap[parentIndex] = element;
32 index = parentIndex;
33 }
34 }
35
36 pop() {
37 const min = this.heap[0];
38 this.heap[0] = this.heap[this.size() - 1];
39 this.heap.pop();
40 this.bubble_down();
41 return min;
42 }
43
44 bubble_down() {
45 let index = 0;
46 let min = index;
47 const n = this.heap.length;
48
49 while (index < n) {
50 const left = 2 * index + 1;
51 const right = left + 1;
52
53 if (left < n && this.heap[left].priority < this.heap[min].priority) {
54 min = left;
55 }
56 if (right < n && this.heap[right].priority < this.heap[min].priority) {
57 min = right;
58 }
59 if (min === index) break;
60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]];
61 index = min;
62 }
63 }
64
65 peek() {
66 return this.heap[0];
67 }
68
69 size() {
70 return this.heap.length;
71 }
72}
73
74function fun(arr) {
75 const heap = new MinHeap();
76 for (const x of arr) {
77 heap.push(new HeapItem(x));
78 }
79 const res = [];
80 for (let i = 0; i < 3; i++) {
81 res.push(heap.pop().item);
82 }
83 return res;
84}
85
Recommended Readings
https algomonster s3 us east 2 amazonaws com 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
https algomonster s3 us east 2 amazonaws com 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
LeetCode 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 we
Want a Structured Path to Master System Design Too? Don’t Miss This!