ret = new ArrayList<>();
- for (int i = 1; i < this.size + 1; i++) {
- if (identity[i] == id) {
- ret.add(i);
- }
- }
- return ret;
- }
-}
diff --git a/src/main/java/dataStructures/disjointSet/weightedUnion/DisjointSet.java b/src/main/java/dataStructures/disjointSet/weightedUnion/DisjointSet.java
new file mode 100644
index 00000000..929028da
--- /dev/null
+++ b/src/main/java/dataStructures/disjointSet/weightedUnion/DisjointSet.java
@@ -0,0 +1,123 @@
+package dataStructures.disjointSet.weightedUnion;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+
+/**
+ * Implementation of weighted-union structure;
+ * Turns a list of objects into a data structure that supports union operations.
+ *
+ * Note that implementation below includes path compression. Refer to README for more details
+ *
+ * @param generic type of object to be stored
+ */
+public class DisjointSet {
+ private final Map parents;
+ private final Map size;
+
+ /**
+ * Basic constructor to initialize Disjoint Set structure using weighted union concept.
+ */
+ public DisjointSet() {
+ parents = new HashMap<>();
+ size = new HashMap<>();
+ }
+
+ /**
+ * Constructor to initialize Disjoint Set structure with a known list of objects.
+ * @param objects
+ */
+ public DisjointSet(List objects) {
+ parents = new HashMap<>();
+ size = new HashMap<>();
+ for (int i = 0; i < objects.size(); i++) {
+ T obj = objects.get(i);
+ parents.put(obj, obj); // initially, every object forms a tree, with itself as the root
+ size.put(obj, 1); // each tree has size 1 at the start
+ }
+ }
+
+ /**
+ * Internal helper method to find the root (identifier) of an object. Note that path compression has been included.
+ * A point of concern might be performing path compression would require updating the sizes tracked by each node
+ * for consistency's sake. But doing so does not affect correctness of algorithm.
+ * Because all the algorithm needs is the correct size of each subtree to decide on how to union
+ * and shifting descendants around does not affect size of subtree.
+ * @param obj
+ * @return the root of the subtree.
+ */
+ private T findRoot(T obj) {
+ while (!obj.equals(parents.get(obj))) {
+ T parent = parents.get(obj);
+ // START OF PATH COMPRESSION
+ T grandParent = parents.get(parent);
+ parents.put(obj, grandParent); // powerful one-liner to reduce the height of trees every traversal
+ // END
+ obj = parent;
+ }
+ return obj;
+ }
+
+ public int size() {
+ return parents.size();
+ }
+
+ /**
+ * Adds an object into the structure.
+ * @param obj
+ */
+ public void add(T obj) {
+ parents.put(obj, obj);
+ size.put(obj, 1);
+ }
+
+ /**
+ * Checks if object a and object b are in the same component.
+ * @param a
+ * @param b
+ * @return
+ */
+ public boolean find(T a, T b) {
+ T rootOfA = findRoot(a);
+ T rootOfB = findRoot(b);
+ return rootOfA.equals(rootOfB);
+ }
+
+ /**
+ * Merge the components of object a and object b.
+ * @param a
+ * @param b
+ */
+ public void union(T a, T b) {
+ T rootOfA = findRoot(a);
+ T rootOfB = findRoot(b);
+ int sizeA = size.get(rootOfA);
+ int sizeB = size.get(rootOfB);
+
+ if (sizeA < sizeB) {
+ parents.put(rootOfA, rootOfB); // update root A to be child of root B
+ size.put(rootOfB, size.get(rootOfB) + size.get(rootOfA)); // update size of bigger tree
+ } else {
+ parents.put(rootOfB, rootOfA); // update root B to be child of root A
+ size.put(rootOfA, size.get(rootOfA) + size.get(rootOfB)); // update size of bigger tree
+ }
+ }
+
+ /**
+ * Retrieves all elements that are in the same component as the specified object. Not a typical operation
+ * but here to illustrate other use case.
+ * @param a
+ * @return a list of objects
+ */
+ public List retrieveFromSameComponent(T a) {
+ List ret = new ArrayList<>();
+ for (T obj : parents.keySet()) {
+ if (find(a, obj)) {
+ ret.add(obj);
+ }
+ }
+ return ret;
+ }
+}
diff --git a/src/main/java/dataStructures/disjointSet/weightedUnion/README.md b/src/main/java/dataStructures/disjointSet/weightedUnion/README.md
index cc9be4b4..42e50a54 100644
--- a/src/main/java/dataStructures/disjointSet/weightedUnion/README.md
+++ b/src/main/java/dataStructures/disjointSet/weightedUnion/README.md
@@ -4,15 +4,15 @@
Here, we consider a completely different approach. We consider the use of trees. Every element can be
thought of as a tree node and starts off in its own component. Under this representation, it is likely
that at any given point, we might have a forest of trees, and that's perfectly fine. The root node of each tree
-simply represents the component / set of all elements in the same set.
+simply represents the identity / is a representative of all elements in the same component.
Note that the trees here are not necessarily binary trees. In fact, more often than not, we will have nodes
with multiple children nodes.
### Union
Between the two components, decide on the component to represent the combined set as before.
Now, union is simply assigning the root node of one tree to be the child of the root node of another. Hence, its name.
-One thing to note is that to identify the component of the object involves traversing to the root node of the
-tree.
+Identifying the component of the object involves traversing to the root node of the tree. Also, note that
+union operations **can result in a forest**.
### Find
For each of the node, we traverse up the tree from the current node until the root. Check if the
@@ -26,13 +26,39 @@ are balanced.
**Space**: O(n), implementation still involves wrapping the n elements with some structure / wrapper (e.g. Node class).
# Weighted Union
+
## Background
Now, we improve upon the Quick Union structure by ensuring trees constructed are 'balanced'. Balanced
trees have a nice property that the height of the tree will be upper-bounded by O(log(n)). This considerably speeds
up Union operations.
-We additionally track the size of each tree and ensure that whenever there is a union between 2 elements, the smaller
-tree will be the child of a larger tree.
-It can be mathematically (induction) shown the height of the tree is bounded by O(log(n)).
+We additionally track the size of each tree and ensure that whenever there is a union between 2 elements, **the smaller
+tree becomes a child of the larger tree.**
+It can be mathematically shown the height of the tree is bounded by O(log(n)).
+
+
+

+
+ Credits: CS2040s Lecture Slides
+
+
+### Intuition - Why It Works
+First, it is crucial to know that Weighted Union's efficiency relies on careful **construction** of the trees.
+Every element / object starts off in its own tree (i.e. its own component). When two components are merged, the smaller
+objects of the smaller tree becomes part of the larger tree (by setting the root node of the smaller tree as a child).
+
Note that if the trees are of the same size, it does not matter which is assigned.
+
+Notice that trees will only increase in height when it's size is doubled. Working on this intuition, one can show
+(by induction) that a tree of height h has at least 2^h elements. Consequently,
+**a tree of size n is at most height of logn**.
+
+### Implementation Details
+The concept introduces the idea of constructing trees and forests and certainly, one can similarly implement a
+Node wrapper class to represent objects as nodes in a tree.
+But notice that the operations only need knowledge of the parent node and the size of the tree
+(which is tracked by the root). In other words, using internal lists and arrays to track is sufficient to
+simulate the construction of trees.
+
+Our implementation does this.
## Complexity Analysis
**Time**: O(log(n)) for Union and Find operations.
@@ -52,3 +78,9 @@ Interested readers can find out more [here](https://dl.acm.org/doi/pdf/10.1145/3
**Space**: O(n)
## Notes
+### Sample Demo - LeetCode 684: Redundant Connections
+The 'objects' in the question are given to be integers. Using int arrays instead of HashMap mapping in our
+implementation would suffice. But below uses the code exactly from our implementation to show its versatility.
+
+
+
diff --git a/src/test/java/dataStructures/disjointSet/quickFind/DisjointSetTest.java b/src/test/java/dataStructures/disjointSet/quickFind/DisjointSetTest.java
new file mode 100644
index 00000000..7d0c1695
--- /dev/null
+++ b/src/test/java/dataStructures/disjointSet/quickFind/DisjointSetTest.java
@@ -0,0 +1,78 @@
+package dataStructures.disjointSet.quickFind;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class DisjointSetTest {
+ @Test
+ public void construct_shouldCorrectlyInitializeEmpty() {
+ DisjointSet ds = new DisjointSet<>();
+ Assert.assertEquals(ds.size(), 0);
+ }
+
+ @Test
+ public void construct_shouldCorrectlyInitializeNonEmpty() {
+ List lst = Arrays.asList("andre", "chang xian", "jun neng", "kai ting", "shu heng");
+
+ DisjointSet ds = new DisjointSet<>(lst);
+ Assert.assertEquals(ds.size(), 5);
+
+ Assert.assertFalse(ds.find("andre", "kai ting"));
+ }
+
+ @Test
+ public void find_shouldCorrectlyFindItself() {
+ List lst = Arrays.asList("andre", "chang xian", "jun neng");
+
+ DisjointSet ds = new DisjointSet<>(lst);
+ Assert.assertTrue(ds.find("chang xian", "chang xian"));
+ }
+
+ @Test
+ public void union_shouldCorrectlyUpdate() {
+ List lst = Arrays.asList("andre", "chang xian", "jun neng", "kai ting", "shu heng");
+
+ DisjointSet ds = new DisjointSet<>(lst);
+
+ Assert.assertFalse(ds.find("andre", "kai ting"));
+
+ ds.union("andre", "kai ting");
+ Assert.assertTrue(ds.find("andre", "kai ting"));
+ Assert.assertFalse(ds.find("andre", "chang xian"));
+ Assert.assertFalse(ds.find("andre", "shu heng"));
+ Assert.assertFalse(ds.find("jun neng", "kai ting"));
+ }
+
+ @Test
+ public void retrieve_shouldCorrectlyRetrieveComponents() {
+ List lst = Arrays.asList("andre", "chang xian", "jun neng", "kai ting", "shu heng", "seth", "gilbert");
+
+ DisjointSet ds = new DisjointSet<>(lst);
+ ds.union("andre", "kai ting");
+ ds.union("chang xian", "jun neng");
+ ds.union("jun neng", "gilbert");
+ ds.union("chang xian", "seth");
+
+ List resultA = ds.retrieveFromSameComponent("kai ting");
+ Collections.sort(resultA);
+ List expectedA = Arrays.asList("andre", "kai ting");
+ Collections.sort(expectedA);
+ Assert.assertEquals(expectedA, resultA);
+
+ List resultB = ds.retrieveFromSameComponent("gilbert");
+ Collections.sort(resultB);
+ List expectedB = Arrays.asList("chang xian", "jun neng", "seth", "gilbert");
+ Collections.sort(expectedB);
+ Assert.assertEquals(expectedB, resultB);
+
+ List resultC = ds.retrieveFromSameComponent("shu heng");
+ Collections.sort(resultC);
+ List expectedC = Arrays.asList("shu heng");
+ Collections.sort(expectedC);
+ Assert.assertEquals(expectedC, resultC);
+ }
+}
diff --git a/src/test/java/dataStructures/disjointSet/weightedUnion/DisjointSetTest.java b/src/test/java/dataStructures/disjointSet/weightedUnion/DisjointSetTest.java
new file mode 100644
index 00000000..4e923091
--- /dev/null
+++ b/src/test/java/dataStructures/disjointSet/weightedUnion/DisjointSetTest.java
@@ -0,0 +1,78 @@
+package dataStructures.disjointSet.weightedUnion;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class DisjointSetTest {
+ @Test
+ public void construct_shouldCorrectlyInitializeEmpty() {
+ DisjointSet ds = new DisjointSet<>();
+ Assert.assertEquals(ds.size(), 0);
+ }
+
+ @Test
+ public void construct_shouldCorrectlyInitializeNonEmpty() {
+ List lst = Arrays.asList("andre", "chang xian", "jun neng", "kai ting", "shu heng");
+
+ DisjointSet ds = new DisjointSet<>(lst);
+ Assert.assertEquals(ds.size(), 5);
+
+ Assert.assertFalse(ds.find("andre", "kai ting"));
+ }
+
+ @Test
+ public void find_shouldCorrectlyFindItself() {
+ List lst = Arrays.asList("andre", "chang xian", "jun neng");
+
+ DisjointSet ds = new DisjointSet<>(lst);
+ Assert.assertTrue(ds.find("chang xian", "chang xian"));
+ }
+
+ @Test
+ public void union_shouldCorrectlyUpdate() {
+ List lst = Arrays.asList("andre", "chang xian", "jun neng", "kai ting", "shu heng");
+
+ DisjointSet ds = new DisjointSet<>(lst);
+
+ Assert.assertFalse(ds.find("andre", "kai ting"));
+
+ ds.union("andre", "kai ting");
+ Assert.assertTrue(ds.find("andre", "kai ting"));
+ Assert.assertFalse(ds.find("andre", "chang xian"));
+ Assert.assertFalse(ds.find("andre", "shu heng"));
+ Assert.assertFalse(ds.find("jun neng", "kai ting"));
+ }
+
+ @Test
+ public void retrieve_shouldCorrectlyRetrieveComponents() {
+ List lst = Arrays.asList("andre", "chang xian", "jun neng", "kai ting", "shu heng", "seth", "gilbert");
+
+ DisjointSet ds = new DisjointSet<>(lst);
+ ds.union("andre", "kai ting");
+ ds.union("chang xian", "jun neng");
+ ds.union("jun neng", "gilbert");
+ ds.union("chang xian", "seth");
+
+ List resultA = ds.retrieveFromSameComponent("kai ting");
+ Collections.sort(resultA);
+ List expectedA = Arrays.asList("andre", "kai ting");
+ Collections.sort(expectedA);
+ Assert.assertEquals(expectedA, resultA);
+
+ List resultB = ds.retrieveFromSameComponent("gilbert");
+ Collections.sort(resultB);
+ List expectedB = Arrays.asList("chang xian", "jun neng", "seth", "gilbert");
+ Collections.sort(expectedB);
+ Assert.assertEquals(expectedB, resultB);
+
+ List resultC = ds.retrieveFromSameComponent("shu heng");
+ Collections.sort(resultC);
+ List expectedC = Arrays.asList("shu heng");
+ Collections.sort(expectedC);
+ Assert.assertEquals(expectedC, resultC);
+ }
+}