diff --git a/src/main/java/com/thealgorithms/datastructures/disjointsetunion/DisjointSetUnionBySize.java b/src/main/java/com/thealgorithms/datastructures/disjointsetunion/DisjointSetUnionBySize.java new file mode 100644 index 000000000000..71951f67dfc8 --- /dev/null +++ b/src/main/java/com/thealgorithms/datastructures/disjointsetunion/DisjointSetUnionBySize.java @@ -0,0 +1,83 @@ +package com.thealgorithms.datastructures.disjointsetunion; + +/** + * Disjoint Set Union (DSU) with Union by Size. + * This data structure tracks a set of elements partitioned into disjoint (non-overlapping) subsets. + * It supports two primary operations efficiently: + * + * + * + * Union by size always attaches the smaller tree under the root of the larger tree. + * This helps keep the tree shallow, improving the efficiency of find operations. + * + * @see Disjoint Set Union (Wikipedia) + */ +public class DisjointSetUnionBySize { + /** + * Node class for DSU by size. + * Each node keeps track of its parent and the size of the set it represents. + */ + public static class Node { + public T value; + public Node parent; + public int size; // size of the set + + public Node(T value) { + this.value = value; + this.parent = this; + this.size = 1; // initially, the set size is 1 + } + } + + /** + * Creates a new disjoint set containing the single specified element. + * @param value the element to be placed in a new singleton set + * @return a node representing the new set + */ + public Node makeSet(final T value) { + return new Node<>(value); + } + + /** + * Finds and returns the representative (root) of the set containing the given node. + * This method applies path compression to flatten the tree structure for future efficiency. + * @param node the node whose set representative is to be found + * @return the representative (root) node of the set + */ + public Node findSet(Node node) { + if (node != node.parent) { + node.parent = findSet(node.parent); // path compression + } + return node.parent; + } + + /** + * Merges the sets containing the two given nodes using union by size. + * The root of the smaller set is attached to the root of the larger set. + * @param x a node in the first set + * @param y a node in the second set + */ + public void unionSets(Node x, Node y) { + Node rootX = findSet(x); + Node rootY = findSet(y); + + if (rootX == rootY) { + return; // They are already in the same set + } + // Union by size: attach smaller tree under the larger one + if (rootX.size < rootY.size) { + rootX.parent = rootY; + rootY.size += rootX.size; // update size + } else { + rootY.parent = rootX; + rootX.size += rootY.size; // update size + } + } +} +// This implementation uses union by size instead of union by rank. +// The size field tracks the number of elements in each set. +// When two sets are merged, the smaller set is always attached to the larger set's root. +// This helps keep the tree shallow and improves the efficiency of find operations. diff --git a/src/test/java/com/thealgorithms/datastructures/disjointsetunion/DisjointSetUnionBySizeTest.java b/src/test/java/com/thealgorithms/datastructures/disjointsetunion/DisjointSetUnionBySizeTest.java new file mode 100644 index 000000000000..71dade9796dc --- /dev/null +++ b/src/test/java/com/thealgorithms/datastructures/disjointsetunion/DisjointSetUnionBySizeTest.java @@ -0,0 +1,146 @@ +package com.thealgorithms.datastructures.disjointsetunion; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +public class DisjointSetUnionBySizeTest { + + @Test + public void testMakeSet() { + DisjointSetUnionBySize dsu = new DisjointSetUnionBySize<>(); + DisjointSetUnionBySize.Node node = dsu.makeSet(1); + assertNotNull(node); + assertEquals(node, node.parent); + assertEquals(1, node.size); + } + + @Test + public void testUnionFindSet() { + DisjointSetUnionBySize dsu = new DisjointSetUnionBySize<>(); + DisjointSetUnionBySize.Node node1 = dsu.makeSet(1); + DisjointSetUnionBySize.Node node2 = dsu.makeSet(2); + DisjointSetUnionBySize.Node node3 = dsu.makeSet(3); + DisjointSetUnionBySize.Node node4 = dsu.makeSet(4); + + dsu.unionSets(node1, node2); + dsu.unionSets(node3, node2); + dsu.unionSets(node3, node4); + dsu.unionSets(node1, node3); + + DisjointSetUnionBySize.Node root1 = dsu.findSet(node1); + DisjointSetUnionBySize.Node root2 = dsu.findSet(node2); + DisjointSetUnionBySize.Node root3 = dsu.findSet(node3); + DisjointSetUnionBySize.Node root4 = dsu.findSet(node4); + + assertEquals(root1, root2); + assertEquals(root1, root3); + assertEquals(root1, root4); + assertEquals(4, root1.size); + } + + @Test + public void testFindSetOnSingleNode() { + DisjointSetUnionBySize dsu = new DisjointSetUnionBySize<>(); + DisjointSetUnionBySize.Node node = dsu.makeSet("A"); + assertEquals(node, dsu.findSet(node)); + } + + @Test + public void testUnionAlreadyConnectedNodes() { + DisjointSetUnionBySize dsu = new DisjointSetUnionBySize<>(); + DisjointSetUnionBySize.Node node1 = dsu.makeSet(1); + DisjointSetUnionBySize.Node node2 = dsu.makeSet(2); + DisjointSetUnionBySize.Node node3 = dsu.makeSet(3); + + dsu.unionSets(node1, node2); + dsu.unionSets(node2, node3); + + // Union nodes that are already connected + dsu.unionSets(node1, node3); + + // All should have the same root + DisjointSetUnionBySize.Node root = dsu.findSet(node1); + assertEquals(root, dsu.findSet(node2)); + assertEquals(root, dsu.findSet(node3)); + assertEquals(3, root.size); + } + + @Test + public void testMultipleMakeSets() { + DisjointSetUnionBySize dsu = new DisjointSetUnionBySize<>(); + DisjointSetUnionBySize.Node node1 = dsu.makeSet(1); + DisjointSetUnionBySize.Node node2 = dsu.makeSet(2); + DisjointSetUnionBySize.Node node3 = dsu.makeSet(3); + + assertNotEquals(node1, node2); + assertNotEquals(node2, node3); + assertNotEquals(node1, node3); + + assertEquals(node1, node1.parent); + assertEquals(node2, node2.parent); + assertEquals(node3, node3.parent); + assertEquals(1, node1.size); + assertEquals(1, node2.size); + assertEquals(1, node3.size); + } + + @Test + public void testPathCompression() { + DisjointSetUnionBySize dsu = new DisjointSetUnionBySize<>(); + DisjointSetUnionBySize.Node node1 = dsu.makeSet(1); + DisjointSetUnionBySize.Node node2 = dsu.makeSet(2); + DisjointSetUnionBySize.Node node3 = dsu.makeSet(3); + + dsu.unionSets(node1, node2); + dsu.unionSets(node2, node3); + + // After findSet, path compression should update parent to root directly + DisjointSetUnionBySize.Node root = dsu.findSet(node3); + assertEquals(root, node1); + assertEquals(node1, node3.parent); + assertEquals(3, root.size); + } + + @Test + public void testMultipleDisjointSets() { + DisjointSetUnionBySize dsu = new DisjointSetUnionBySize<>(); + DisjointSetUnionBySize.Node node1 = dsu.makeSet(1); + DisjointSetUnionBySize.Node node2 = dsu.makeSet(2); + DisjointSetUnionBySize.Node node3 = dsu.makeSet(3); + DisjointSetUnionBySize.Node node4 = dsu.makeSet(4); + DisjointSetUnionBySize.Node node5 = dsu.makeSet(5); + DisjointSetUnionBySize.Node node6 = dsu.makeSet(6); + + // Create two separate components + dsu.unionSets(node1, node2); + dsu.unionSets(node2, node3); + + dsu.unionSets(node4, node5); + dsu.unionSets(node5, node6); + + // Verify they are separate + assertEquals(dsu.findSet(node1), dsu.findSet(node2)); + assertEquals(dsu.findSet(node2), dsu.findSet(node3)); + assertEquals(dsu.findSet(node4), dsu.findSet(node5)); + assertEquals(dsu.findSet(node5), dsu.findSet(node6)); + + assertNotEquals(dsu.findSet(node1), dsu.findSet(node4)); + assertNotEquals(dsu.findSet(node3), dsu.findSet(node6)); + } + + @Test + public void testEmptyValues() { + DisjointSetUnionBySize dsu = new DisjointSetUnionBySize<>(); + DisjointSetUnionBySize.Node emptyNode = dsu.makeSet(""); + DisjointSetUnionBySize.Node nullNode = dsu.makeSet(null); + + assertEquals(emptyNode, dsu.findSet(emptyNode)); + assertEquals(nullNode, dsu.findSet(nullNode)); + + dsu.unionSets(emptyNode, nullNode); + assertEquals(dsu.findSet(emptyNode), dsu.findSet(nullNode)); + } +}