Problem
In this problem, a tree is an undirected graph that is connected and has no cycles.
You are given a graph that started as a tree with n
nodes labeled from 1
to n
, with one additional edge added. The added edge has two different vertices chosen from 1
to n
, and was not an edge that already existed. The graph is represented as an array edges
of length n
where edges[i] = [ai, bi]
indicates that there is an edge between nodes ai
and bi
in the graph.
Return an edge that can be removed so that the resulting graph is a tree of n
nodes. If there are multiple answers, return the answer that occurs last in the input.
Examples
Example 1:
graph LR 1 --- 2 1 --- 3 2 --- 3
Input: edges =[[1,2],[1,3],[2,3]]
Output: [2,3]
Example 2:
graph LR 1 --> 2 & 4 & 5 2 --> 3 3 --> 4
Input: edges =[[1,2],[2,3],[3,4],[1,4],[1,5]]
Output: [1,4]
Solution
Method 1 - Union Find
We can iterate through edges and find both vertices of the edge to see if they belong to the same segment. If so, we’ve located our redundant edge and can return it. If not, we should merge the two different segments with union.
Code
Java
class Solution {
public int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
UnionFind uf = new UnionFind(n);
for (int[] edge: edges) {
int u = edge[0];
int v = edge[1];
if (!uf.union(u - 1, v - 1)) {
return new int[] {u, v};
}
}
return new int[]{};
}
static class UnionFind {
private int[] parent;
private int[] rank;
private int numComponents;
public UnionFind(int n) {
this.numComponents = n;
makeSet();
}
private void makeSet() {
rank = new int[numComponents];
parent = new int[numComponents];
for (int i = 0; i < parent.length; i++) {
parent[i] = i;
rank[i] = 0;
}
}
public boolean union(int x, int y) {
int root1 = find(x);
int root2 = find(y);
if (root1 == root2) {
return false;
}
if (rank[root1] > rank[root2]) {
parent[root2] = root1;
} else {
parent[root1] = root2;
}
if (rank[root1] == rank[root2]) {
rank[root2] += 1;
}
numComponents--;
return true;
}
public int find(int x) {
while (parent[x] != x) {
x = parent[x];
}
return x;
}
public int getNumComponents() {
return numComponents;
}
public boolean isFullyConnected() {
return numComponents == 1;
}
}
}
Complexity
- ⏰ Time complexity:
O(n * α(n))
, whereN
is number of vertices in the graph- Explanation: Using both path compression and union by size ensures that the amortized time per UnionFind operation is only
α(n)
, which is optimal, whereα(n)
is the inverse Ackermann function. This function has a valueα(n) < 5
for any value of n that can be written in this physical universe, so the disjoint-set operations take place in essentially constant time. Reference: https://en.wikipedia.org/wiki/Disjoint-set_data_structure
- Explanation: Using both path compression and union by size ensures that the amortized time per UnionFind operation is only
- 🧺 Space complexity:
O(n)
as we store parent and rank arrays of sizen
in Union-Find data structure.
Method 2 - Using Bridges and Articulation Points
Although it’s less well-known than Union Find and DFS, this problem can be effectively addressed using Tarjan’s Bridge finding algorithm. By capturing the low
and disc
values for each node during traversal, we can identify any bridges.
In this problem, our goal is to find the last edge that is not a bridge. I incorporated a somewhat awkward swap logic since the bridge check is conducted post-traversal, but overall, this remains a standard implementation of the Bridge finding algorithm.
Code
Java
class Solution {
int time = 1;
public int[] findRedundantConnection(int[][] edges) {
int n = edges.length;
int[] low = new int[n + 1];
int[] disc = new int[n + 1];
List<List<Integer>> adjList = buildGraph(edges);
dfs(adjList, 1, low, disc, -1);
for (int i = n - 1; i >= 0; i--) {
int[] edge = edges[i];
int u = edge[0];
int v = edge[1];
// Swap since u must always be the earlier node during our check
if (disc[u] > disc[v]) {
int temp = u;
u = v;
v = temp;
}
if (low[v] > disc[u]){
// We have bridge
continue;
} else {
return edge;
}
}
return null;
}
private List<List<Integer>> buildGraph(int[][] edges) {
int n = edges.length;
List<List<Integer>> adjList = new ArrayList<>();
for (int i = 0; i <= n; i++) {
adjList.add(new ArrayList<>());
}
for (int[] edge: edges) {
int src = edge[0];
int dst = edge[1];
adjList.get(src).add(dst);
adjList.get(dst).add(src);
}
return adjList;
}
private void dfs(List<List<Integer>> adjList, int u, int[] low, int[] disc, int parent) {
if (disc[u] != 0) {
return;
}
disc[u] = time;
low[u] = time;
time++;
for (int v: adjList.get(u)) {
if (v == parent) {
continue;
}
if (disc[v] == 0) {
dfs(adjList, v, low, disc, u);
low[u] = Math.min(low[u], low[v]);
} else {
low[u] = Math.min(low[u], disc[v]);
}
}
}
}