Start learning free

LESSON 492 OF 569 · GRAPHS

Redundant Connection in Java: Union-Find with Path Halving

Practice O(n · α(n)) Read the full explanation

A tree is an undirected graph that is connected (you can get from any node to any other by following edges) and has no cycle. Put another way, there is exactly one path between any two nodes.

You start with a tree of n nodes, numbered from 1 to n, and one extra edge is added to it. The extra edge joins two different nodes that were not joined by an edge before. The graph you get is given as an array edges of length n, where edges[i] = [ai, bi] means that there is an edge between nodes ai and bi.

Return an edge that can be removed so that the other n - 1 edges form a tree of all n nodes. If several edges work, return the one that comes last in edges.

 

Example 1:

Input: edges = [[1, 2], [1, 3], [2, 3]]
Output: [2, 3]
Explanation: The three edges form the cycle 1 - 2 - 3 - 1:
      1
     / \
    2 - 3
Removing any one of them leaves a tree. [2, 3] is the one that comes last.

Example 2:

Input: edges = [[1, 2], [2, 3], [3, 4], [1, 4], [1, 5]]
Output: [1, 4]
Explanation: The cycle is 1 - 2 - 3 - 4 - 1:
    5 - 1 - 2
        |   |
        4 - 3
[1, 5] comes after [1, 4] in the input, but removing it would cut node 5 off from the others.
Of the four edges on the cycle, [1, 4] comes last.

Example 3:

Input: edges = [[1, 2], [3, 4], [2, 3], [4, 5], [1, 5]]
Output: [1, 5]
Explanation: When [2, 3] comes, nodes 2 and 3 have both been seen already, but in two
separate groups, {1, 2} and {3, 4}: the edge joins them. All five edges form the cycle
1 - 2 - 3 - 4 - 5 - 1, and [1, 5] comes last.

 

Constraints:

  • n == edges.length
  • 3 <= n <= 1000
  • edges[i].length == 2
  • 1 <= ai < bi <= n
  • There are no repeated edges.
  • The graph is connected.

Note:

Your algorithm should run in O(n·α(n)) time, nearly O(n), and use O(n) extra space.

Trying each edge from the last one back, removing it and checking with a DFS whether the rest is still connected, costs O(n2). The tutorial adds the edges one by one, in the order they are given, with union-find, as in Graph Valid Tree: an edge whose two nodes are already in the same group closes a cycle. The graph has exactly one cycle, and removing any of its edges leaves a tree, while removing an edge that is not on it cuts the tree in two. The cycle closes when the last of its edges comes, because all its other edges came before, so the first edge that closes a cycle is the answer. With path halving and union by size, each step costs O(α(n)) amortized, where α is the inverse Ackermann function, below 5 for any n a computer can hold.

STEP 1 OF 21
    Ctrl + Enter to check

    JAVA · GRAPHS

    Redundant Connection in Java, explained

    Problem overview

    A tree is a connected undirected graph with no cycle. You get a tree of n nodes, numbered 1 to n, plus one extra edge between two nodes that were not joined before. The method findRedundantConnection receives the int[][] named edges, which has n edges, and returns an edge whose removal leaves a tree of all n nodes. When several edges would work, it returns the one that comes last in edges.

    For [[1,2],[1,3],[2,3]] the three edges form the cycle 1 - 2 - 3 - 1. Removing any of them leaves a tree, and [2, 3] comes last, so that is the answer. For [[1,2],[2,3],[3,4],[1,4],[1,5]] the cycle is 1 - 2 - 3 - 4 - 1, and the answer is [1, 4]. The last edge of the input, [1, 5], is not on the cycle: removing it would cut node 5 off.

    Approach: add edges one by one and watch for a cycle

    The brute force tries each edge from the last backwards, removes it and runs a depth-first search to see whether the rest is still connected. That costs O(n²).

    A faster way is to add the edges in the order given and keep track of which nodes are already connected. An edge whose two ends are already in the same group closes a cycle. The graph has exactly one cycle, and removing any edge of the cycle leaves a tree, while removing an edge that is not on it splits the tree. The cycle closes when its last edge arrives, because all its other edges came earlier. So the first edge that joins two already-connected nodes is the cycle's last edge in input order, and that is the answer, even if more edges follow it.

    The data structure for "are these two nodes already connected?" is union-find (also called disjoint sets). Each group is stored as a tree: every node points to a parent, and the root, a node that is its own parent, names the group. Two nodes are in the same group exactly when they lead to the same root. Two improvements keep the trees shallow:

    • Union by size. When two groups merge, the smaller tree goes under the larger one's root, so a node gets deeper only when its group at least doubles.
    • Path halving. While walking up to a root, each node is pointed at its grandparent, so later walks are shorter.

    With both, each operation costs O(α(n)) amortized, where α is the inverse Ackermann function, below 5 for any size a computer can hold.

    On [[1,2],[3,4],[2,3],[4,5],[1,5]] the edges [1, 2] and [3, 4] make the groups {1, 2} and {3, 4}. Edge [2, 3] finds the two different roots 1 and 3 and merges them, and [4, 5] adds node 5. At [1, 5] both ends have root 1, so that edge closes the cycle, and the method returns [1, 5]. After the four merges the parent array is [0, 1, 1, 1, 1, 1]: every node points straight to node 1.

    Step by step

    1. Store the edge count in n. A tree of n nodes has n - 1 edges, and the extra edge makes n, so the count is also the number of nodes.
    2. Create parent with n + 1 entries. Nodes start at 1, so node i sits at index i and index 0 is never used.
    3. Create size the same way. size[r] counts the nodes in the group whose root is r; only roots' entries are kept up to date.
    4. Loop over the indexes and give every node its own group: parent[i] = i and size[i] = 1. A new int array holds zeros, so the sizes must be set.
    5. Loop over the edges in the given order, since the order decides which cycle edge is returned.
    6. Find both roots with the helper find, into rootA and rootB.
    7. If the roots are equal, return the edge. The ends were already connected, so this edge closes the cycle.
    8. Make rootA the larger group's root. If size[rootA] < size[rootB], swap them through a temporary variable.
    9. Merge by setting parent[rootB] = rootA and adding size[rootB] to size[rootA].
    10. Return new int[0] after the loop. The input always has an extra edge, so the method returns inside the loop and this line is never reached; Java requires a return on every path.
    11. Write find: while parent[x] != x, set parent[x] = parent[parent[x]] (path halving) and then x = parent[x]. When the loop stops, x is the root, and the method returns it.

    The solution in Java

    class Solution {
        public int[] findRedundantConnection(int[][] edges) {
            int n = edges.length;
            int[] parent = new int[n + 1];
            int[] size = new int[n + 1];
            for (int i = 0; i <= n; i++) {
                parent[i] = i;
                size[i] = 1;
            }
            for (int[] edge : edges) {
                int rootA = find(parent, edge[0]);
                int rootB = find(parent, edge[1]);
                if (rootA == rootB) {
                    return edge;
                }
                if (size[rootA] < size[rootB]) {
                    int temp = rootA;
                    rootA = rootB;
                    rootB = temp;
                }
                parent[rootB] = rootA;
                size[rootA] += size[rootB];
            }
            return new int[0];
        }
    
        int find(int[] parent, int x) {
            while (parent[x] != x) {
                parent[x] = parent[parent[x]];
                x = parent[x];
            }
            return x;
        }
    }

    Java details: arrays are passed by reference, so when find changes parent, the caller sees it. That is intended, because path halving updates the shared structure. Assigning to the parameter x changes only the local copy of the number. The method returns edge, the very int[] from the input, not a copy. find uses a loop instead of recursion, so a long chain cannot overflow the call stack. For a chain 5 - 4 - 3 - 2 - 1 - 0 stored in parent as [0, 0, 1, 2, 3, 4], one call find(parent, 5) returns 0 and leaves parent as [0, 0, 1, 1, 3, 3]: the visited nodes now skip a level.

    Complexity

    Each of the n edges costs two find calls and at most one merge, each O(α(n)) amortized, so the time is O(n · α(n)), which is nearly O(n). The two arrays take O(n) extra space.

    Edge cases and common mistakes

    • Returning the last edge of the input. That is correct only if it is on the cycle. On the second example the input's last edge is [1, 5], but the answer is [1, 4].
    • Arrays of size n. Node n would be out of range. Use n + 1.
    • Forgetting to start every node as its own root. With zeros in parent, every node would appear to share root 0.
    • Merging nodes instead of roots. Set the parent of a root, not of the edge's endpoint, or the groups are not merged properly.
    • A long chain. A path of 1000 nodes plus the edge [1, 1000] returns [1, 1000].

    Testing

    These inputs were run against the solution above:

    edgesResult
    [[1,2],[1,3],[2,3]][2, 3]
    [[1,2],[2,3],[3,4],[1,4],[1,5]][1, 4]
    [[1,2],[3,4],[2,3],[4,5],[1,5]][1, 5]
    Keep going with Java500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free