Solution: Redundant Connection
Let's solve the Redundant Connection problem using the Union Find pattern.
Statement
We’re given an undirected graph consisting of nodes. The graph is represented as an array called edges
, of length , where edges[i] = [a, b]
indicates that there is an edge between nodes a
and b
in the graph.
Return an edge that can be removed to make the graph a edges
.
Constraints:
edges.length
edges[i].length
2-
a
b
- There are no repeated edges.
- The given graph is connected.
- The graph contains only one cycle.
Solution
So far, you have probably brainstormed some approaches and have an idea of how to solve this problem. Let’s explore some of these approaches and figure out which one to follow based on considerations such as time complexity and any implementation constraints.
Naive approach
We can solve this problem using techniques like DFS or BFS, however, the naive approach using DFS or BFS has a time complexity of in the worst-case scenario. This is because the DFS or BFS algorithm will explore the graph by visiting each node and its edges, which may result in visiting many nodes multiple times, and not necessarily finding the redundant connection in an efficient manner.
Optimized approach using union find
We can use the union find pattern which is known to detect cycles within a graph efficiently. We iterate through the given edges of the graph. For each edge, we check if its two nodes are already connected in the union find structure using the find method. If they're not connected, we connect the two nodes using the union by rank method. If they are connected, adding this edge would create a cycle, and thus, it's the redundant connection we're looking for, so we return it.
The slides below illustrate how the algorithm runs:
Note: In the following section, we will gradually build the solution. Alternatively, you can skip straight to just the code.
Step by step solution construction
We start with the basic implementation of Union Find (without the use of rank or path compression):
File: UnionFind.java
In the UnionFind
class, we declare the parent
array with length based on the edges
array.
File: main.java
In the redundantConnection
function, we’ll declare an object of the given class and initialize the parent
array with default values.
import java.util.*;class UnionFind {public int[] parent;// Constructorpublic UnionFind(int n) {parent = new int[n + 1];for (int i = 0; i <= n; i++) {parent[i] = i;}}// Function to find which subset a particular element belongs topublic int find(int v) {if (parent[v] != v) {return find(parent[v]);}return v;}// Function to join two subsets into a single subsetpublic void union(int v1, int v2) {int p1 = find(v1);int p2 = find(v2);if (p1 != p2) {parent[p1] = p2;;}}}
File: UnionFind.java
Next, we need to check if the vertices forming an edge have the same parent. In the union
function, we’ll call the parent of the first vertex p1
and the parent of the second vertex p2
. The algorithm is described as follows:
-
If both
v1
andv2
have the same parent, i.e.,p1
is equal top2
, the given edge is redundant, so we return FALSE. -
Otherwise, this edge is connecting two vertices that were not already connected. So, we’ll update the
parent
list by making a connection based on the current edge and then return TRUE.
File: main.java
In the redundantConnection
function, we traverse the edges
array and for each edge, we attempt to connect its two vertices, v1
, and v2
, through the union(v1, v2)
method.
import java.util.*;class UnionFind {public int[] parent;// Constructorpublic UnionFind(int n) {parent = new int[n + 1];for (int i = 0; i <= n; i++) {parent[i] = i;}}// Function to find which subset a particular element belongs to// Returns FALSE if both vertices have the same parent, otherwise, updates the parent and rank lists by making a connection based on the passed edge// Returns TRUE if no cycle exits in the graphpublic int find(int v) {if (parent[v] != v) {return find(parent[v]);}return v;}// Function to join two subsets into a single subsetpublic boolean union(int v1, int v2) {System.out.println("\n\n\tChecking if the vertices have the same parent");System.out.println("\t\tEdge: [" + v1 + ", " + v2 + "]");System.out.println("\t\tFirst vertex: " + v1);System.out.println("\t\tSecond vertex: " + v2);// Find the root parents of v1 and v2int p1 = find(v1);int p2 = find(v2);System.out.println("\t\t\tParent of the first vertex: " + p1);System.out.println("\t\t\tParent of the second vertex: " + p2);// If both parents are the same, a cycle exists and v1,v2 is the redundant edgeif (p1 == p2) {System.out.println("\t\tSince both vertices have the same parent, this is a redundant edge");return false;}// Updates the parent list otherwiseelse {System.out.println("\t\tThe vertices don't have the same parent, updating parent array");System.out.print("\t\t\tparent: " + Arrays.toString(parent) + " ⟶ ");parent[p1] = p2;;System.out.println(Arrays.toString(parent));}return true;}}
We will now update the Union Find algorithm by adding both union by rank and path compression methods. We’ll make the following changes to the UnionFind
class:
Union by rank:
-
We initialize a
rank
array (set to the length of theedges
array) with s. -
In the
union
function, if the parents,p1
andp2
of the vertices are not the same, we make the connection based on the ranks of both parents. This is done in the following way:-
If
p1
's rank is greater thanp2
's rank, we’ll updatep2
's parent withp1
, and addp2
's rank top1
's rank. For example, ifrank[p1]
andrank[p2]
, we’ll addp2
top1
's set and update its rank to . -
Similarly, if
p2
's rank is greater thanp1
's rank, we’ll updatep1
's parent withp2
, and addp1
's rank top2
's rank.
-
Path compression:
- In the
find
function, for a nodev
, we make the found root as the parent ofv
so that we don’t have to traverse all the intermediate nodes again on furtherfind
operations.
import java.util.*;class UnionFind {public int[] parent;public int[] rank;// Constructorpublic UnionFind(int n) {parent = new int[n + 1];rank = new int[n + 1];for (int i = 0; i <= n; i++) {parent[i] = i;rank[i] = 1;}}// Function to find which subset a particular element belongs to// Returns FALSE if both vertices have the same parent, otherwise, updates the parent and rank lists by making a connection based on the passed edge// Returns TRUE if no cycle exits in the graphpublic int find(int v) {if (parent[v] != v) {parent[v] = find(parent[v]);}return parent[v];}// Function to join two subsets into a single subsetpublic boolean union(int v1, int v2) {System.out.println("\n\n\tChecking if the vertices have the same parent");System.out.println("\t\tEdge: [" + v1 + ", " + v2 + "]");System.out.println("\t\tFirst vertex: " + v1);System.out.println("\t\tSecond vertex: " + v2);// Find the root parents of v1 and v2int p1 = find(v1);int p2 = find(v2);System.out.println("\t\t\tParent of the first vertex: " + p1);System.out.println("\t\t\tParent of the second vertex: " + p2);// If both parents are the same, a cycle exists and v1,v2 is the redundant edgeif (p1 == p2) {System.out.println("\t\tSince both vertices have the same parent, this is a redundant edge");return false;}// Updates the parent and rank lists otherwiseelse if (rank[p1] > rank[p2]) {System.out.println("\t\tThe vertices don't have the same parent, updating parent and rank arrays");System.out.print("\t\t\tparent: " + Arrays.toString(parent) + " ⟶ ");parent[p2] = p1;System.out.println(Arrays.toString(parent));System.out.print("\t\t\trank: " + Arrays.toString(rank) + " ⟶ ");rank[p1] += rank[p2];System.out.println(Arrays.toString(rank));}else {System.out.println("\t\tThe vertices don't have the same parent, updating parent and rank arrays");System.out.print("\t\t\tparent: " + Arrays.toString(parent) + " ⟶ ");parent[p1] = p2;System.out.println(Arrays.toString(parent));rank[p2] += rank[p1];System.out.print("\t\t\trank: " + Arrays.toString(rank) + " ⟶ ");System.out.println(Arrays.toString(rank));}return true;}}
Lastly, we need to return the redundant edge. While traversing the edges
array, for each edge, we will check if the union(v1, v2)
method returns FALSE:
-
If it does, both
v1
andv2
will have the same parent. So the current edge will be redundant, and we return it. -
Otherwise,
v1
andv2
have different parents, so we connect them.
import java.util.*;class RedundantConnections {public static int[] redundantConnection(int[][] edges) {// Declaring the parent and rank list with lengths based on the edges arraySystem.out.println("\n\tDeclaring the parent and rank arrays");UnionFind connections = new UnionFind(edges.length);System.out.println("\t\tparent: " + Arrays.toString(connections.parent));System.out.println("\t\trank: " + Arrays.toString(connections.rank));// Traversing the edges of the graph to check for the redundant edgefor (int[] edge : edges) {int v1 = edge[0];int v2 = edge[1];if (!connections.union(v1, v2)) {return edge;}}return new int[]{};}// Driver codepublic static void main(String[] args) {int[][][] edges = {{{1, 2}, {1, 3}, {2, 3}},{{1, 2}, {2, 3}, {1, 3}},{{1, 2}, {2, 3}, {3, 4}, {1, 4}, {1, 5}},{{1, 2}, {1, 3}, {1, 4}, {3, 4}, {2, 4}},{{1, 2}, {1, 3}, {1, 4}, {1, 5}, {2, 3}, {2, 4}, {2, 5}}};for (int i = 0; i < edges.length; i++) {System.out.println((i + 1) + ".\tEdges: " + Arrays.deepToString(edges[i]));System.out.println("\n\tThe redundant connection in the graph is: " + Arrays.toString(redundantConnection(edges[i])));System.out.println(new String(new char[100]).replace("\0", "-"));}}}
Just the code
Here's the complete solution to this problem:
import java.util.*;class RedundantConnections {public static int[] redundantConnection(int[][] edges) {UnionFind connections = new UnionFind(edges.length);for (int[] edge : edges) {int v1 = edge[0];int v2 = edge[1];if (!connections.union(v1, v2)) {return edge;}}return new int[]{};}// Driver codepublic static void main(String[] args) {int[][][] edges = {{{1, 2}, {1, 3}, {2, 3}},{{1, 2}, {2, 3}, {1, 3}},{{1, 2}, {2, 3}, {3, 4}, {1, 4}, {1, 5}},{{1, 2}, {1, 3}, {1, 4}, {3, 4}, {2, 4}},{{1, 2}, {1, 3}, {1, 4}, {1, 5}, {2, 3}, {2, 4}, {2, 5}}};for (int i = 0; i < edges.length; i++) {System.out.println((i + 1) + ".\tEdges: " + Arrays.deepToString(edges[i]));System.out.println("\n\tThe redundant connection in the graph is: " + Arrays.toString(redundantConnection(edges[i])));System.out.println(new String(new char[100]).replace("\0", "-"));}}}
Solution summary
To recap, the solution to this problem can be divided into the following two main parts:
-
Initialize
parent
andrank
arrays based on the length of theedges
array. -
Traverse the
edges
array and for each edge, compare the parents of both vertices:-
If the parents are the same, the current edge is redundant, so we return it.
-
Otherwise, we connect the two vertices based on their respective ranks.
-
Time complexity
The time complexity of this solution is , where is the number of edges.
Explanation:
-
We use a loop to traverse the
edges
array, resulting in iterations. -
The time complexity of the the
Union
andFind
functions is , since both path compression and union find by rank are being used. is almost constant time and grows very slowly with the input size, so the time complexity of both these functions can be considered as constant time for practical purposes.
Therefore, the overall time complexity becomes , which simplifies to .
Space Complexity
The space complexity of this solution is .
Level up your interview prep. Join Educative to access 70+ hands-on prep courses.