diff --git a/DIRECTORY.md b/DIRECTORY.md index b311b10fa177..c6b20517eb34 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -348,9 +348,11 @@ - 📄 [Point](src/main/java/com/thealgorithms/geometry/Point.java) - 📁 **graph** - 📄 [ConstrainedShortestPath](src/main/java/com/thealgorithms/graph/ConstrainedShortestPath.java) + - 📄 [DisjointSet](src/main/java/com/thealgorithms/graph/DisjointSet.java) - 📄 [HopcroftKarp](src/main/java/com/thealgorithms/graph/HopcroftKarp.java) - 📄 [PredecessorConstrainedDfs](src/main/java/com/thealgorithms/graph/PredecessorConstrainedDfs.java) - 📄 [StronglyConnectedComponentOptimized](src/main/java/com/thealgorithms/graph/StronglyConnectedComponentOptimized.java) + - 📄 [TopologicalSort](src/main/java/com/thealgorithms/graph/TopologicalSort.java) - 📄 [TravelingSalesman](src/main/java/com/thealgorithms/graph/TravelingSalesman.java) - 📁 **greedyalgorithms** - 📄 [ActivitySelection](src/main/java/com/thealgorithms/greedyalgorithms/ActivitySelection.java) diff --git a/src/main/java/com/thealgorithms/graph/DisjointSet.java b/src/main/java/com/thealgorithms/graph/DisjointSet.java new file mode 100644 index 000000000000..d1ad400007ef --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/DisjointSet.java @@ -0,0 +1,185 @@ +package com.thealgorithms.graph; + +import java.util.ArrayList; +import java.util.List; + +/** + * Implementation of the Disjoint Set (Union-Find) data structure with path + * compression + * and union by rank optimizations. + * + *

+ * A disjoint-set data structure maintains a collection of disjoint dynamic + * sets. + * Each set is represented by a "representative" element. The data structure + * supports + * two main operations: + *

+ * + * + *

+ * Time Complexity: + *

+ * + *

+ * where α(n) is the inverse Ackermann function, which grows extremely slowly. + * For all practical values of n, α(n) ≤ 4. + *

+ * + *

+ * Space Complexity: O(n) where n is the number of elements + *

+ * + *

+ * Applications: + *

+ * + */ +public class DisjointSet { + private final int[] parent; + private final int[] rank; + private final int size; + private int numSets; // Tracks number of disjoint sets + + /** + * Initializes a disjoint set data structure with elements from 0 to size-1. + * + * @param size number of elements + * @throws IllegalArgumentException if size is negative + */ + public DisjointSet(int size) { + if (size < 0) { + throw new IllegalArgumentException("Size must be non-negative"); + } + this.size = size; + this.numSets = size; + parent = new int[size]; + rank = new int[size]; + + for (int i = 0; i < size; i++) { + parent[i] = i; + rank[i] = 0; + } + } + + /** + * Finds the representative of the set containing element x. + * Uses path compression to optimize future queries. + * + * @param x element to find representative for + * @return representative of x's set + * @throws IllegalArgumentException if x is out of bounds + */ + public int find(int x) { + if (x < 0 || x >= size) { + throw new IllegalArgumentException("Element out of bounds: " + x); + } + + // Path compression: Make all nodes on path point directly to root + if (parent[x] != x) { + parent[x] = find(parent[x]); + } + return parent[x]; + } + + /** + * Merges the sets containing elements x and y. + * Uses union by rank to keep the tree shallow. + * + * @param x first element + * @param y second element + * @return true if x and y were in different sets, false if they were already in + * same set + * @throws IllegalArgumentException if either element is out of bounds + */ + public boolean union(int x, int y) { + int rootX = find(x); + int rootY = find(y); + + if (rootX == rootY) { + return false; // Already in same set + } + + // Union by rank: Attach smaller rank tree under root of higher rank tree + if (rank[rootX] < rank[rootY]) { + parent[rootX] = rootY; + } else if (rank[rootX] > rank[rootY]) { + parent[rootY] = rootX; + } else { + parent[rootY] = rootX; + rank[rootX]++; + } + + numSets--; + return true; + } + + /** + * Checks if two elements are in the same set. + * + * @param x first element + * @param y second element + * @return true if x and y are in the same set + * @throws IllegalArgumentException if either element is out of bounds + */ + public boolean connected(int x, int y) { + return find(x) == find(y); + } + + /** + * Returns the current number of disjoint sets. + * + * @return number of disjoint sets + */ + public int getNumSets() { + return numSets; + } + + /** + * Returns the size of the set containing element x. + * + * @param x element to find set size for + * @return size of x's set + * @throws IllegalArgumentException if x is out of bounds + */ + public int getSetSize(int x) { + int root = find(x); + int count = 0; + for (int i = 0; i < size; i++) { + if (find(i) == root) { + count++; + } + } + return count; + } + + /** + * Returns all elements in the same set as x. + * + * @param x element to find set members for + * @return list of all elements in x's set + * @throws IllegalArgumentException if x is out of bounds + */ + public List getSetMembers(int x) { + int root = find(x); + List members = new ArrayList<>(); + for (int i = 0; i < size; i++) { + if (find(i) == root) { + members.add(i); + } + } + return members; + } +} diff --git a/src/main/java/com/thealgorithms/graph/TopologicalSort.java b/src/main/java/com/thealgorithms/graph/TopologicalSort.java new file mode 100644 index 000000000000..b6e84c67f051 --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/TopologicalSort.java @@ -0,0 +1,127 @@ +package com.thealgorithms.graph; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +/** + * Implementation of Kahn's algorithm for topological sorting of a directed + * acyclic graph (DAG). + * + *

+ * The algorithm finds a linear ordering of vertices such that for every + * directed edge u -> v, + * vertex u comes before v in the ordering. A topological ordering is possible + * if and only if the + * graph has no directed cycles (i.e., it is a DAG). + *

+ * + *

+ * Time Complexity: O(V + E) where V is the number of vertices and E is the + * number of edges + *

+ *

+ * Space Complexity: O(V) for the queue and in-degree array + *

+ * + *

+ * Applications: + *

+ * + */ +public final class TopologicalSort { + + private TopologicalSort() { + } + + /** + * Performs topological sorting using Kahn's algorithm. + * + * @param numVertices number of vertices in the graph (0 to numVertices-1) + * @param edges list of directed edges, where each edge is represented as + * int[]{from, to} + * @return an array containing the topologically sorted order, or null if a + * cycle exists + * @throws IllegalArgumentException if edges is null or contains invalid + * vertices + */ + public static int[] sort(int numVertices, Iterable edges) { + if (edges == null) { + throw new IllegalArgumentException("Edge list must not be null"); + } + + // Create adjacency list representation + List> graph = new ArrayList<>(numVertices); + for (int i = 0; i < numVertices; i++) { + graph.add(new ArrayList<>()); + } + + // Calculate in-degree for each vertex + int[] inDegree = new int[numVertices]; + for (int[] edge : edges) { + if (edge[0] < 0 || edge[0] >= numVertices || edge[1] < 0 || edge[1] >= numVertices) { + throw new IllegalArgumentException("Invalid vertex in edge: " + Arrays.toString(edge)); + } + graph.get(edge[0]).add(edge[1]); + inDegree[edge[1]]++; + } + + // Initialize queue with vertices having no incoming edges + Queue queue = new LinkedList<>(); + for (int i = 0; i < numVertices; i++) { + if (inDegree[i] == 0) { + queue.offer(i); + } + } + + int[] result = new int[numVertices]; + int index = 0; + + // Process vertices in topological order + while (!queue.isEmpty()) { + int vertex = queue.poll(); + result[index++] = vertex; + + // Reduce in-degree of neighbors and add to queue if in-degree becomes 0 + for (int neighbor : graph.get(vertex)) { + inDegree[neighbor]--; + if (inDegree[neighbor] == 0) { + queue.offer(neighbor); + } + } + } + + // Check if topological sort is possible (no cycles) + return index == numVertices ? result : null; + } + + /** + * Alternative version that returns the result as a List and throws an exception + * if a cycle is detected. + * + * @param numVertices number of vertices in the graph (0 to numVertices-1) + * @param edges list of directed edges, where each edge is represented as + * int[]{from, to} + * @return a list containing the vertices in topologically sorted order + * @throws IllegalArgumentException if the graph contains a cycle or if input is + * invalid + */ + public static List sortAndDetectCycle(int numVertices, List edges) { + int[] result = sort(numVertices, edges); + if (result == null) { + throw new IllegalArgumentException("Graph contains a cycle"); + } + List sorted = new ArrayList<>(numVertices); + for (int vertex : result) { + sorted.add(vertex); + } + return sorted; + } +} diff --git a/src/test/java/com/thealgorithms/graph/DisjointSetTest.java b/src/test/java/com/thealgorithms/graph/DisjointSetTest.java new file mode 100644 index 000000000000..58dc09fc23b5 --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/DisjointSetTest.java @@ -0,0 +1,124 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class DisjointSetTest { + + @Test + @DisplayName("Initially all elements are in separate sets") + void initialState() { + DisjointSet ds = new DisjointSet(5); + assertEquals(5, ds.getNumSets()); + for (int i = 0; i < 5; i++) { + assertEquals(i, ds.find(i)); + assertEquals(1, ds.getSetSize(i)); + } + } + + @Test + @DisplayName("Union of elements creates correct sets") + void basicUnion() { + DisjointSet ds = new DisjointSet(4); + assertTrue(ds.union(0, 1)); // Should succeed + assertTrue(ds.union(2, 3)); // Should succeed + assertFalse(ds.union(0, 1)); // Should fail (already united) + assertEquals(2, ds.getNumSets()); + assertTrue(ds.connected(0, 1)); + assertTrue(ds.connected(2, 3)); + assertFalse(ds.connected(0, 2)); + } + + @Test + @DisplayName("Path compression optimizes tree structure") + void pathCompression() { + DisjointSet ds = new DisjointSet(4); + ds.union(0, 1); + ds.union(1, 2); + ds.union(2, 3); + // After these unions, we should have one set + assertEquals(1, ds.getNumSets()); + // After path compression, all elements should point to the same root + int root = ds.find(0); + for (int i = 1; i < 4; i++) { + assertEquals(root, ds.find(i)); + } + } + + @Test + @DisplayName("Set size tracking works correctly") + void setSizeTracking() { + DisjointSet ds = new DisjointSet(6); + ds.union(0, 1); // Set of size 2 + ds.union(2, 3); // Another set of size 2 + ds.union(0, 2); // Merge into set of size 4 + assertEquals(4, ds.getSetSize(0)); + assertEquals(4, ds.getSetSize(1)); + assertEquals(4, ds.getSetSize(2)); + assertEquals(4, ds.getSetSize(3)); + assertEquals(1, ds.getSetSize(4)); + assertEquals(1, ds.getSetSize(5)); + } + + @Test + @DisplayName("Get set members returns correct lists") + void getSetMembers() { + DisjointSet ds = new DisjointSet(4); + ds.union(0, 1); + ds.union(2, 3); + + List set1 = ds.getSetMembers(0); + List set2 = ds.getSetMembers(2); + + assertEquals(2, set1.size()); + assertTrue(set1.contains(0)); + assertTrue(set1.contains(1)); + + assertEquals(2, set2.size()); + assertTrue(set2.contains(2)); + assertTrue(set2.contains(3)); + } + + @Test + @DisplayName("Out of bounds access throws exception") + void outOfBounds() { + DisjointSet ds = new DisjointSet(3); + assertThrows(IllegalArgumentException.class, () -> ds.find(-1)); + assertThrows(IllegalArgumentException.class, () -> ds.find(3)); + assertThrows(IllegalArgumentException.class, () -> ds.union(-1, 0)); + assertThrows(IllegalArgumentException.class, () -> ds.union(0, 3)); + } + + @Test + @DisplayName("Negative size throws exception") + void negativeSize() { + assertThrows(IllegalArgumentException.class, () -> new DisjointSet(-1)); + } + + @Test + @DisplayName("Connected components example") + void connectedComponents() { + DisjointSet ds = new DisjointSet(6); + // Create two connected components: 0-1-2 and 3-4-5 + ds.union(0, 1); + ds.union(1, 2); + ds.union(3, 4); + ds.union(4, 5); + + assertEquals(2, ds.getNumSets()); + + // Check connectivity within components + assertTrue(ds.connected(0, 2)); + assertTrue(ds.connected(3, 5)); + + // Check separation between components + assertFalse(ds.connected(0, 3)); + assertFalse(ds.connected(2, 4)); + } +} diff --git a/src/test/java/com/thealgorithms/graph/TopologicalSortTest.java b/src/test/java/com/thealgorithms/graph/TopologicalSortTest.java new file mode 100644 index 000000000000..7eed902bdbec --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/TopologicalSortTest.java @@ -0,0 +1,92 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class TopologicalSortTest { + + @Test + @DisplayName("Simple DAG returns correct ordering") + void simpleDAG() { + List edges = new ArrayList<>(); + edges.add(new int[] { 0, 1 }); + edges.add(new int[] { 0, 2 }); + edges.add(new int[] { 1, 3 }); + edges.add(new int[] { 2, 3 }); + int[] result = TopologicalSort.sort(4, edges); + assertArrayEquals(new int[] { 0, 1, 2, 3 }, result); + } + + @Test + @DisplayName("Empty graph returns valid ordering") + void emptyGraph() { + List edges = new ArrayList<>(); + int[] result = TopologicalSort.sort(3, edges); + assertEquals(3, result.length); + // Any permutation is valid for empty graph + assertTrue(Arrays.stream(result).allMatch(v -> v >= 0 && v < 3)); + } + + @Test + @DisplayName("Graph with cycle returns null") + void graphWithCycle() { + List edges = new ArrayList<>(); + edges.add(new int[] { 0, 1 }); + edges.add(new int[] { 1, 2 }); + edges.add(new int[] { 2, 0 }); + assertNull(TopologicalSort.sort(3, edges)); + } + + @Test + @DisplayName("Course prerequisites example") + void coursePrerequisites() { + // Example: Course prerequisites where edge [a,b] means course a must be taken + // before b + List edges = new ArrayList<>(); + edges.add(new int[] { 1, 0 }); // Calculus I -> Calculus II + edges.add(new int[] { 2, 0 }); // Linear Algebra -> Calculus II + edges.add(new int[] { 1, 3 }); // Calculus I -> Differential Equations + edges.add(new int[] { 2, 3 }); // Linear Algebra -> Differential Equations + List result = TopologicalSort.sortAndDetectCycle(4, edges); + // Either [1,2,0,3] or [2,1,0,3] is valid + assertEquals(4, result.size()); + // Verify prerequisites are satisfied + assertTrue(result.indexOf(1) < result.indexOf(0)); // Calc I before Calc II + assertTrue(result.indexOf(2) < result.indexOf(0)); // Linear Algebra before Calc II + assertTrue(result.indexOf(1) < result.indexOf(3)); // Calc I before Diff Eq + assertTrue(result.indexOf(2) < result.indexOf(3)); // Linear Algebra before Diff Eq + } + + @Test + @DisplayName("Invalid vertex throws exception") + void invalidVertex() { + List edges = new ArrayList<>(); + edges.add(new int[] { 0, 5 }); // Vertex 5 is invalid for a graph with 3 vertices + assertThrows(IllegalArgumentException.class, () -> TopologicalSort.sort(3, edges)); + } + + @Test + @DisplayName("Null edge list throws exception") + void nullEdgeList() { + assertThrows(IllegalArgumentException.class, () -> TopologicalSort.sort(3, null)); + } + + @Test + @DisplayName("sortAndDetectCycle throws exception for cyclic graph") + void detectCycleThrowsException() { + List edges = new ArrayList<>(); + edges.add(new int[] { 0, 1 }); + edges.add(new int[] { 1, 2 }); + edges.add(new int[] { 2, 0 }); + assertThrows(IllegalArgumentException.class, () -> TopologicalSort.sortAndDetectCycle(3, edges)); + } +}