diff --git a/README.md b/README.md index d48667b3e..21f337945 100644 --- a/README.md +++ b/README.md @@ -207,10 +207,8 @@ $ java -cp classes com.williamfiset.algorithms.search.BinarySearch - [:movie_camera:](https://www.youtube.com/watch?v=oDqjPvD54Ss) [Breadth first search (adjacency list)](src/main/java/com/williamfiset/algorithms/graphtheory/BreadthFirstSearchAdjacencyList.java) **- O(V+E)** - [Bridges/cut edges (adjacency list)](src/main/java/com/williamfiset/algorithms/graphtheory/BridgesAdjacencyList.java) **- O(V+E)** - [Boruvkas (adjacency list, min spanning tree algorithm)](src/main/java/com/williamfiset/algorithms/graphtheory/Boruvkas.java) **- O(Elog(V))** -- [Find connected components (adjacency list, union find)](src/main/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsAdjacencyList.java) **- O(Elog(E))** -- [Find connected components (adjacency list, DFS)](src/main/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsDfsSolverAdjacencyList.java) **- O(V+E)** -- [Depth first search (adjacency list, iterative)](src/main/java/com/williamfiset/algorithms/graphtheory/DepthFirstSearchAdjacencyListIterative.java) **- O(V+E)** -- [Depth first search (adjacency list, iterative, fast stack)](src/main/java/com/williamfiset/algorithms/graphtheory/DepthFirstSearchAdjacencyListIterativeFastStack.java) **- O(V+E)** +- [Find connected components (adjacency list, union find)](src/main/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsUnionFind.java) **- O(V+E)** +- [Find connected components (adjacency list, DFS)](src/main/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsDfs.java) **- O(V+E)** - [:movie_camera:](https://www.youtube.com/watch?v=7fujbpJ0LB4) [Depth first search (adjacency list, recursive)](src/main/java/com/williamfiset/algorithms/graphtheory/DepthFirstSearchAdjacencyListRecursive.java) **- O(V+E)** - [:movie_camera:](https://www.youtube.com/watch?v=pSqmAO-m7Lk) [Dijkstra's shortest path (adjacency list, lazy implementation)](src/main/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyList.java) **- O(Elog(V))** - [:movie_camera:](https://www.youtube.com/watch?v=pSqmAO-m7Lk) [Dijkstra's shortest path (adjacency list, eager implementation + D-ary heap)](src/main/java/com/williamfiset/algorithms/graphtheory/DijkstrasShortestPathAdjacencyListWithDHeap.java) **- O(ElogE/V(V))** diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/BUILD b/src/main/java/com/williamfiset/algorithms/graphtheory/BUILD index 2351fe8a2..cca226819 100644 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/BUILD +++ b/src/main/java/com/williamfiset/algorithms/graphtheory/BUILD @@ -76,31 +76,17 @@ java_binary( runtime_deps = [":graphtheory"], ) -# bazel run //src/main/java/com/williamfiset/algorithms/graphtheory:ConnectedComponentsAdjacencyList +# bazel run //src/main/java/com/williamfiset/algorithms/graphtheory:ConnectedComponentsUnionFind java_binary( - name = "ConnectedComponentsAdjacencyList", - main_class = "com.williamfiset.algorithms.graphtheory.ConnectedComponentsAdjacencyList", + name = "ConnectedComponentsUnionFind", + main_class = "com.williamfiset.algorithms.graphtheory.ConnectedComponentsUnionFind", runtime_deps = [":graphtheory"], ) -# bazel run //src/main/java/com/williamfiset/algorithms/graphtheory:ConnectedComponentsDfsSolverAdjacencyList +# bazel run //src/main/java/com/williamfiset/algorithms/graphtheory:ConnectedComponentsDfs java_binary( - name = "ConnectedComponentsDfsSolverAdjacencyList", - main_class = "com.williamfiset.algorithms.graphtheory.ConnectedComponentsDfsSolverAdjacencyList", - runtime_deps = [":graphtheory"], -) - -# bazel run //src/main/java/com/williamfiset/algorithms/graphtheory:DepthFirstSearchAdjacencyListIterative -java_binary( - name = "DepthFirstSearchAdjacencyListIterative", - main_class = "com.williamfiset.algorithms.graphtheory.DepthFirstSearchAdjacencyListIterative", - runtime_deps = [":graphtheory"], -) - -# bazel run //src/main/java/com/williamfiset/algorithms/graphtheory:DepthFirstSearchAdjacencyListIterativeFastStack -java_binary( - name = "DepthFirstSearchAdjacencyListIterativeFastStack", - main_class = "com.williamfiset.algorithms.graphtheory.DepthFirstSearchAdjacencyListIterativeFastStack", + name = "ConnectedComponentsDfs", + main_class = "com.williamfiset.algorithms.graphtheory.ConnectedComponentsDfs", runtime_deps = [":graphtheory"], ) diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/Boruvkas.java b/src/main/java/com/williamfiset/algorithms/graphtheory/Boruvkas.java index ff3dd82f2..32f32c476 100644 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/Boruvkas.java +++ b/src/main/java/com/williamfiset/algorithms/graphtheory/Boruvkas.java @@ -1,10 +1,34 @@ +/** + * Boruvka's Minimum Spanning Tree Algorithm — Edge List + * + *

Finds the MST of a weighted undirected graph by repeatedly selecting the + * cheapest outgoing edge from each connected component and merging components. + * + *

Algorithm: + *

    + *
  1. Start with each node as its own component (using Union-Find).
  2. + *
  3. For each component, find the minimum-weight edge crossing to another component.
  4. + *
  5. Add all such cheapest edges to the MST and merge the components.
  6. + *
  7. Repeat until only one component remains, or no more merges are possible.
  8. + *
+ * + *

If the graph is disconnected, no MST exists and the solver returns null. + * + *

Time: O(E log V) + *

Space: O(V + E) + * + * @author William Fiset, william.alexandre.fiset@gmail.com + */ package com.williamfiset.algorithms.graphtheory; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.OptionalLong; public class Boruvkas { - static class Edge implements Comparable { + static class Edge { int u, v, cost; public Edge(int u, int v, int cost) { @@ -12,145 +36,127 @@ public Edge(int u, int v, int cost) { this.v = v; this.cost = cost; } - - @Override - public String toString() { - return String.format("%d %d, cost: %d", u, v, cost); - } - - @Override - public int compareTo(Edge other) { - int cmp = cost - other.cost; - // Break ties by picking lexicographically smallest edge pair. - if (cmp == 0) { - cmp = u - other.u; - if (cmp == 0) return v - other.v; - return cmp; - } - return cmp; - } } - // Inputs - private final int n; // Number of nodes - private final Edge[] graph; // Edge list - - // Internal + private final int n; + private final Edge[] graph; private boolean solved; private boolean mstExists; - - // Outputs private long minCostSum; private List mst; public Boruvkas(int n, Edge[] graph) { - if (graph == null) throw new IllegalArgumentException(); + if (graph == null) { + throw new IllegalArgumentException(); + } this.graph = graph; this.n = n; + this.mst = new ArrayList<>(); } - // Returns the edges used in finding the minimum spanning tree, or returns - // null if no MST exists. - public List getMst() { + /** + * Returns the edges in the MST, or empty if the graph is disconnected. + */ + public Optional> getMst() { solve(); - return mstExists ? mst : null; + return mstExists ? Optional.of(mst) : Optional.empty(); } - public Long getMstCost() { + /** + * Returns the total cost of the MST, or empty if the graph is disconnected. + */ + public OptionalLong getMstCost() { solve(); - return mstExists ? minCostSum : null; + return mstExists ? OptionalLong.of(minCostSum) : OptionalLong.empty(); } - // Given a graph represented as an edge list this method finds - // the Minimum Spanning Tree (MST) cost if there exists - // a MST, otherwise it returns null. private void solve() { - if (solved) return; + if (solved) { + return; + } - mst = new ArrayList<>(); UnionFind uf = new UnionFind(n); while (uf.components > 1) { - boolean stop = true; Edge[] cheapest = new Edge[n]; - // Find the cheapest edge for each component + // For each edge, track the cheapest crossing edge for each component. for (Edge e : graph) { int root1 = uf.find(e.u); int root2 = uf.find(e.v); - if (root1 == root2) continue; - + if (root1 == root2) { + continue; + } if (cheapest[root1] == null || e.cost < cheapest[root1].cost) { cheapest[root1] = e; - stop = false; } if (cheapest[root2] == null || e.cost < cheapest[root2].cost) { cheapest[root2] = e; - stop = false; } } - if (stop) break; - - // Add the cheapest edges to the MST - for (int i = 0; i < n; i++) { - Edge e = cheapest[i]; - if (e == null) { - continue; - } - int root1 = uf.find(e.u); - int root2 = uf.find(e.v); - if (root1 != root2) { - uf.union(root1, root2); + // Merge components using their cheapest crossing edges. + int prevComponents = uf.components; + for (Edge e : cheapest) { + if (e != null && uf.find(e.u) != uf.find(e.v)) { + uf.union(e.u, e.v); mst.add(e); minCostSum += e.cost; } } + + if (uf.components == prevComponents) { + break; + } } mstExists = (mst.size() == n - 1); solved = true; } + // ==================== Main ==================== + + // + // 1 7 2 + // 0 --------------- 1 --------------- 2 --------------- 3 + // | | | | + // | | | | + // 4 | 3 | 5 | 6 | + // | | | | + // | | | | + // 4 --------------- 5 --------------- 6 --------------- 7 + // 8 2 9 + // + // MST cost: 23 + // public static void main(String[] args) { - - int n = 10, m = 18, i = 0; - Edge[] g = new Edge[m]; - - // Edges are treated as undirected - g[i++] = new Edge(0, 1, 5); - g[i++] = new Edge(0, 3, 4); - g[i++] = new Edge(0, 4, 1); - g[i++] = new Edge(1, 2, 4); - g[i++] = new Edge(1, 3, 2); - g[i++] = new Edge(2, 7, 4); - g[i++] = new Edge(2, 8, 1); - g[i++] = new Edge(2, 9, 2); - g[i++] = new Edge(3, 6, 11); - g[i++] = new Edge(3, 7, 2); - g[i++] = new Edge(4, 3, 2); - g[i++] = new Edge(4, 5, 1); - g[i++] = new Edge(5, 3, 5); - g[i++] = new Edge(5, 6, 7); - g[i++] = new Edge(6, 7, 1); - g[i++] = new Edge(6, 8, 4); - g[i++] = new Edge(7, 8, 6); - g[i++] = new Edge(9, 8, 0); - - Boruvkas solver = new Boruvkas(n, g); - - Long ans = solver.getMstCost(); - if (ans != null) { - System.out.println("MST cost: " + ans); - for (Edge e : solver.getMst()) { - System.out.println(e); + Edge[] g = { + new Edge(0, 1, 1), + new Edge(1, 2, 7), + new Edge(2, 3, 2), + new Edge(0, 4, 4), + new Edge(1, 5, 3), + new Edge(2, 6, 5), + new Edge(3, 7, 6), + new Edge(4, 5, 8), + new Edge(5, 6, 2), + new Edge(6, 7, 9), + }; + + Boruvkas solver = new Boruvkas(8, g); + + OptionalLong cost = solver.getMstCost(); + if (cost.isPresent()) { + System.out.println("MST cost: " + cost.getAsLong()); // 23 + for (Edge e : solver.getMst().get()) { + System.out.printf("Edge %d-%d, cost: %d%n", e.u, e.v, e.cost); } } else { System.out.println("No MST exists"); } } - // Union find data structure + // Union-Find with path compression and union by size. private static class UnionFind { int components; int[] id, sz; @@ -166,27 +172,17 @@ public UnionFind(int n) { } public int find(int p) { - int root = p; - while (root != id[root]) root = id[root]; - while (p != root) { // Do path compression - int next = id[p]; - id[p] = root; - p = next; + if (id[p] == p) { + return p; } - return root; - } - - public boolean connected(int p, int q) { - return find(p) == find(q); - } - - public int size(int p) { - return sz[find(p)]; + return id[p] = find(id[p]); } public void union(int p, int q) { int root1 = find(p), root2 = find(q); - if (root1 == root2) return; + if (root1 == root2) { + return; + } if (sz[root1] < sz[root2]) { sz[root2] += sz[root1]; id[root1] = root2; diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsAdjacencyList.java b/src/main/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsAdjacencyList.java deleted file mode 100644 index 907406426..000000000 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsAdjacencyList.java +++ /dev/null @@ -1,167 +0,0 @@ -/** - * This file contains an algorithm to find all the connected components of an undirected graph. If - * the graph you're dealing with is directed have a look at Tarjan's algorithm to find strongly - * connected components. - * - *

The approach I will use to find all the strongly connected components is to use a union find - * data structure to merge together nodes connected by an edge. An alternative approach would be to - * do a breadth first search from each node (except the ones already visited of course) to determine - * the individual components. - * - * @author William Fiset, william.alexandre.fiset@gmail.com - */ -package com.williamfiset.algorithms.graphtheory; - -import java.util.*; - -public class ConnectedComponentsAdjacencyList { - - static class Edge { - int from, to, cost; - - public Edge(int from, int to, int cost) { - this.from = from; - this.to = to; - this.cost = cost; - } - } - - static int countConnectedComponents(Map> graph, int n) { - - UnionFind uf = new UnionFind(n); - - for (int i = 0; i < n; i++) { - List edges = graph.get(i); - if (edges != null) { - for (Edge edge : edges) { - uf.unify(edge.from, edge.to); - } - } - } - - return uf.components(); - } - - // Finding connected components example - public static void main(String[] args) { - - final int numNodes = 7; - Map> graph = new HashMap<>(); - - // Setup a graph with four connected components - // namely: {0,1,2}, {3,4}, {5}, {6} - addUndirectedEdge(graph, 0, 1, 1); - addUndirectedEdge(graph, 1, 2, 1); - addUndirectedEdge(graph, 2, 0, 1); - addUndirectedEdge(graph, 3, 4, 1); - addUndirectedEdge(graph, 5, 5, 1); // Self loop - - int components = countConnectedComponents(graph, numNodes); - System.out.printf("Number of components: %d\n", components); - } - - // Helper method to setup graph - private static void addUndirectedEdge( - Map> graph, int from, int to, int cost) { - List list = graph.get(from); - if (list == null) { - list = new ArrayList(); - graph.put(from, list); - } - list.add(new Edge(from, to, cost)); - list.add(new Edge(to, from, cost)); - } -} - -// Union find data structure -class UnionFind { - - // The number of elements in this union find - private int size; - - // Used to track the sizes of each of the components - private int[] sz; - - // id[i] points to the parent of i, if id[i] = i then i is a root node - private int[] id; - - // Tracks the number of components in the union find - private int numComponents; - - public UnionFind(int size) { - - if (size <= 0) throw new IllegalArgumentException("Size <= 0 is not allowed"); - - this.size = numComponents = size; - sz = new int[size]; - id = new int[size]; - - for (int i = 0; i < size; i++) { - id[i] = i; // Link to itself (self root) - sz[i] = 1; // Each component is originally of size one - } - } - - // Find which component/set 'p' belongs to, takes amortized constant time. - public int find(int p) { - - // Find the root of the component/set - int root = p; - while (root != id[root]) root = id[root]; - - // Compress the path leading back to the root. - // Doing this operation is called "path compression" - // and is what gives us amortized constant time complexity. - while (p != root) { - int next = id[p]; - id[p] = root; - p = next; - } - - return root; - } - - // Return whether or not the elements 'p' and - // 'q' are in the same components/set. - public boolean connected(int p, int q) { - return find(p) == find(q); - } - - // Return the size of the components/set 'p' belongs to - public int componentSize(int p) { - return sz[find(p)]; - } - - // Return the number of elements in this UnionFind/Disjoint set - public int size() { - return size; - } - - // Returns the number of remaining components/sets - public int components() { - return numComponents; - } - - // Unify the components/sets containing elements 'p' and 'q' - public void unify(int p, int q) { - - int root1 = find(p); - int root2 = find(q); - - // These elements are already in the same group! - if (root1 == root2) return; - - // Merge smaller component/set into the larger one. - if (sz[root1] < sz[root2]) { - sz[root2] += sz[root1]; - id[root1] = root2; - } else { - sz[root1] += sz[root2]; - id[root2] = root1; - } - - // Since the roots found are different we know that the - // number of components/sets has decreased by one - numComponents--; - } -} diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsDfs.java b/src/main/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsDfs.java new file mode 100644 index 000000000..4417e46ad --- /dev/null +++ b/src/main/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsDfs.java @@ -0,0 +1,136 @@ +/** + * Connected Components — Adjacency List (DFS) + * + *

Finds all connected components of an undirected graph using depth-first + * search. Each unvisited node starts a new DFS that labels every reachable node + * with the same component id. + * + *

For directed graphs, use Tarjan's or Kosaraju's algorithm to find + * strongly connected components instead. + * + *

Time: O(V + E) + *

Space: O(V) + * + * @author William Fiset, william.alexandre.fiset@gmail.com + */ +package com.williamfiset.algorithms.graphtheory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class ConnectedComponentsDfs { + + private final int n; + private final List> graph; + private boolean solved; + private int componentCount; + private int[] componentIds; + + public ConnectedComponentsDfs(List> graph) { + if (graph == null) { + throw new IllegalArgumentException(); + } + this.n = graph.size(); + this.graph = graph; + } + + /** + * Returns the number of connected components. + */ + public int countComponents() { + solve(); + return componentCount; + } + + /** + * Returns the component id of the given node. Component ids are 0-indexed. + */ + public int componentId(int node) { + solve(); + return componentIds[node]; + } + + /** + * Returns the component id array where {@code componentIds[i]} is the + * component id of node i. Component ids are 0-indexed. + */ + public int[] getComponentIds() { + solve(); + return componentIds; + } + + private void solve() { + if (solved) { + return; + } + + componentIds = new int[n]; + Arrays.fill(componentIds, -1); + + for (int i = 0; i < n; i++) { + if (componentIds[i] == -1) { + dfs(i, componentCount++); + } + } + + solved = true; + } + + private void dfs(int at, int id) { + componentIds[at] = id; + for (int to : graph.get(at)) { + if (componentIds[to] == -1) { + dfs(to, id); + } + } + } + + // ==================== Main ==================== + + // + // 0 --- 1 3 --- 6 + // | / | | | + // | / | | | + // 7 2 - 5 4 9 10 + // + // 8 + // + // Components: {0,1,2,5,7}, {3,4,6,9}, {8}, {10} + // Count: 4 + // + public static void main(String[] args) { + int n = 11; + List> graph = createGraph(n); + + addUndirectedEdge(graph, 0, 1); + addUndirectedEdge(graph, 1, 7); + addUndirectedEdge(graph, 7, 0); + addUndirectedEdge(graph, 1, 2); + addUndirectedEdge(graph, 2, 5); + addUndirectedEdge(graph, 3, 4); + addUndirectedEdge(graph, 3, 6); + addUndirectedEdge(graph, 6, 9); + + ConnectedComponentsDfs solver = new ConnectedComponentsDfs(graph); + + System.out.printf("Number of components: %d%n", solver.countComponents()); + + for (int i = 0; i < n; i++) { + System.out.printf("Node %d -> component %d%n", i, solver.componentId(i)); + } + } + + private static List> createGraph(int n) { + List> graph = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + graph.add(new ArrayList<>()); + } + return graph; + } + + private static void addUndirectedEdge(List> graph, int from, int to) { + graph.get(from).add(to); + graph.get(to).add(from); + } +} diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsDfsSolverAdjacencyList.java b/src/main/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsDfsSolverAdjacencyList.java deleted file mode 100644 index 827945121..000000000 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsDfsSolverAdjacencyList.java +++ /dev/null @@ -1,113 +0,0 @@ -/** - * This file contains an algorithm to find all the connected components of an undirected graph. If - * the graph you're dealing with is directed have a look at Tarjan's algorithm to find strongly - * connected components. - * - * @author William Fiset, william.alexandre.fiset@gmail.com - */ -package com.williamfiset.algorithms.graphtheory; - -import java.util.*; - -public class ConnectedComponentsDfsSolverAdjacencyList { - - private final int n; - - private int componentCount; - private int[] components; - private boolean solved; - private boolean[] visited; - private List> graph; - - /** - * @param graph - An undirected graph as an adjacency list. - */ - public ConnectedComponentsDfsSolverAdjacencyList(List> graph) { - if (graph == null) throw new NullPointerException(); - this.n = graph.size(); - this.graph = graph; - } - - public int[] getComponents() { - solve(); - return components; - } - - public int countComponents() { - solve(); - return componentCount; - } - - public void solve() { - if (solved) return; - - visited = new boolean[n]; - components = new int[n]; - for (int i = 0; i < n; i++) { - if (!visited[i]) { - componentCount++; - dfs(i); - } - } - - solved = true; - } - - private void dfs(int at) { - visited[at] = true; - components[at] = componentCount; - for (int to : graph.get(at)) if (!visited[to]) dfs(to); - } - - /* Finding connected components example */ - - public static List> createGraph(int n) { - List> graph = new ArrayList<>(n); - for (int i = 0; i < n; i++) graph.add(new ArrayList<>()); - return graph; - } - - public static void addUndirectedEdge(List> graph, int from, int to) { - graph.get(from).add(to); - graph.get(to).add(from); - } - - public static void main(String[] args) { - - final int n = 11; - List> graph = createGraph(n); - - // Setup a graph with five connected components: - // {0,1,7}, {2,5}, {4,8}, {3,6,9}, {10} - addUndirectedEdge(graph, 0, 1); - addUndirectedEdge(graph, 1, 7); - addUndirectedEdge(graph, 7, 0); - addUndirectedEdge(graph, 2, 5); - addUndirectedEdge(graph, 4, 8); - addUndirectedEdge(graph, 3, 6); - addUndirectedEdge(graph, 6, 9); - - ConnectedComponentsDfsSolverAdjacencyList solver; - solver = new ConnectedComponentsDfsSolverAdjacencyList(graph); - int count = solver.countComponents(); - System.out.printf("Number of components: %d\n", count); - - int[] components = solver.getComponents(); - for (int i = 0; i < n; i++) - System.out.printf("Node %d is part of component %d\n", i, components[i]); - - // Prints: - // Number of components: 5 - // Node 0 is part of component 1 - // Node 1 is part of component 1 - // Node 2 is part of component 2 - // Node 3 is part of component 3 - // Node 4 is part of component 4 - // Node 5 is part of component 2 - // Node 6 is part of component 3 - // Node 7 is part of component 1 - // Node 8 is part of component 4 - // Node 9 is part of component 3 - // Node 10 is part of component 5 - } -} diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsUnionFind.java b/src/main/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsUnionFind.java new file mode 100644 index 000000000..984c28617 --- /dev/null +++ b/src/main/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsUnionFind.java @@ -0,0 +1,162 @@ +/** + * Connected Components — Adjacency List (Union-Find) + * + *

Finds all connected components of an undirected graph using a Union-Find + * data structure. Each edge merges the two endpoint components; the final number + * of disjoint sets is the component count. + * + *

For directed graphs, use Tarjan's or Kosaraju's algorithm to find + * strongly connected components instead. + * + *

Time: O(V + E * α(V)) ≈ O(V + E) + *

Space: O(V) + * + * @author William Fiset, william.alexandre.fiset@gmail.com + */ +package com.williamfiset.algorithms.graphtheory; + +import java.util.ArrayList; +import java.util.List; + +public class ConnectedComponentsUnionFind { + + private final int n; + private final List> graph; + private boolean solved; + private UnionFind uf; + + public ConnectedComponentsUnionFind(List> graph) { + if (graph == null) { + throw new IllegalArgumentException(); + } + this.n = graph.size(); + this.graph = graph; + } + + /** + * Returns the number of connected components. + */ + public int countComponents() { + solve(); + return uf.components; + } + + /** + * Returns the component id (root node) of the given node. + */ + public int componentId(int node) { + solve(); + return uf.find(node); + } + + /** + * Returns the size of the component that the given node belongs to. + */ + public int componentSize(int node) { + solve(); + return uf.sz[uf.find(node)]; + } + + private void solve() { + if (solved) { + return; + } + + uf = new UnionFind(n); + for (int u = 0; u < n; u++) { + for (int v : graph.get(u)) { + uf.union(u, v); + } + } + + solved = true; + } + + // ==================== Main ==================== + + // + // 0 --- 1 3 --- 6 + // | / | | + // | / | | + // 2 4 9 + // + // 5 7 --- 8 10 + // + // Components: {0,1,2}, {3,4,6,9}, {5}, {7,8}, {10} + // Count: 5 + // + public static void main(String[] args) { + int n = 11; + List> graph = createGraph(n); + + addUndirectedEdge(graph, 0, 1); + addUndirectedEdge(graph, 0, 2); + addUndirectedEdge(graph, 1, 2); + addUndirectedEdge(graph, 3, 4); + addUndirectedEdge(graph, 3, 6); + addUndirectedEdge(graph, 6, 9); + addUndirectedEdge(graph, 7, 8); + + ConnectedComponentsUnionFind solver = + new ConnectedComponentsUnionFind(graph); + + System.out.printf("Number of components: %d%n", solver.countComponents()); + + for (int i = 0; i < n; i++) { + System.out.printf("Node %d -> component %d%n", i, solver.componentId(i)); + } + } + + private static List> createGraph(int n) { + List> graph = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + graph.add(new ArrayList<>()); + } + return graph; + } + + private static void addUndirectedEdge(List> graph, int from, int to) { + graph.get(from).add(to); + graph.get(to).add(from); + } + + // Union-Find with path compression and union by size. + private static class UnionFind { + int components; + private final int[] id; + private final int[] sz; + + UnionFind(int n) { + components = n; + id = new int[n]; + sz = new int[n]; + for (int i = 0; i < n; i++) { + id[i] = i; + sz[i] = 1; + } + } + + int find(int p) { + if (id[p] == p) { + return p; + } + return id[p] = find(id[p]); + } + + void union(int p, int q) { + int root1 = find(p); + int root2 = find(q); + if (root1 == root2) { + return; + } + if (sz[root1] < sz[root2]) { + sz[root2] += sz[root1]; + id[root1] = root2; + } else { + sz[root1] += sz[root2]; + id[root2] = root1; + } + components--; + } + } +} diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/DepthFirstSearchAdjacencyListIterative.java b/src/main/java/com/williamfiset/algorithms/graphtheory/DepthFirstSearchAdjacencyListIterative.java deleted file mode 100644 index a9ba446b5..000000000 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/DepthFirstSearchAdjacencyListIterative.java +++ /dev/null @@ -1,95 +0,0 @@ -/** - * An implementation of a iterative DFS with an adjacency list Time Complexity: O(V + E) - * - * @author William Fiset, william.alexandre.fiset@gmail.com - */ -package com.williamfiset.algorithms.graphtheory; - -import java.util.*; - -public class DepthFirstSearchAdjacencyListIterative { - - static class Edge { - int from, to, cost; - - public Edge(int from, int to, int cost) { - this.from = from; - this.to = to; - this.cost = cost; - } - } - - // Perform a depth first search on a graph with n nodes - // from a starting point to count the number of nodes - // in a given component. - static int dfs(Map> graph, int start, int n) { - - int count = 0; - boolean[] visited = new boolean[n]; - Stack stack = new Stack<>(); - - // Start by visiting the starting node - stack.push(start); - visited[start] = true; - - while (!stack.isEmpty()) { - int node = stack.pop(); - count++; - List edges = graph.get(node); - - if (edges != null) { - for (Edge edge : edges) { - if (!visited[edge.to]) { - stack.push(edge.to); - visited[edge.to] = true; - } - } - } - } - - return count; - } - - // Example usage of DFS - public static void main(String[] args) { - - // Create a fully connected graph - // (0) - // / \ - // 5 / \ 4 - // / \ - // 10 < -2 > - // +->(2)<------(1) (4) - // +--- \ / - // \ / - // 1 \ / 6 - // > < - // (3) - int numNodes = 5; - Map> graph = new HashMap<>(); - addDirectedEdge(graph, 0, 1, 4); - addDirectedEdge(graph, 0, 2, 5); - addDirectedEdge(graph, 1, 2, -2); - addDirectedEdge(graph, 1, 3, 6); - addDirectedEdge(graph, 2, 3, 1); - addDirectedEdge(graph, 2, 2, 10); // Self loop - - long nodeCount = dfs(graph, 0, numNodes); - System.out.println("DFS node count starting at node 0: " + nodeCount); - if (nodeCount != 4) System.err.println("Error with DFS"); - - nodeCount = dfs(graph, 4, numNodes); - System.out.println("DFS node count starting at node 4: " + nodeCount); - if (nodeCount != 1) System.err.println("Error with DFS"); - } - - // Helper method to setup graph - private static void addDirectedEdge(Map> graph, int from, int to, int cost) { - List list = graph.get(from); - if (list == null) { - list = new ArrayList(); - graph.put(from, list); - } - list.add(new Edge(from, to, cost)); - } -} diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/DepthFirstSearchAdjacencyListIterativeFastStack.java b/src/main/java/com/williamfiset/algorithms/graphtheory/DepthFirstSearchAdjacencyListIterativeFastStack.java deleted file mode 100644 index 3914181b7..000000000 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/DepthFirstSearchAdjacencyListIterativeFastStack.java +++ /dev/null @@ -1,134 +0,0 @@ -/** - * An implementation of a iterative DFS with an adjacency list using a custom stack for extra speed. - * Time Complexity: O(V + E) - * - * @author William Fiset, william.alexandre.fiset@gmail.com - */ -package com.williamfiset.algorithms.graphtheory; - -import java.util.*; - -// This file contains an implementation of an integer only stack which is -// extremely quick and lightweight. In terms of performance it can outperform -// java.util.ArrayDeque (Java's fastest stack implementation) by a factor of 50! -// However, the downside is you need to know an upper bound on the number of -// elements that will be inside the stack at any given time for it to work correctly. -class IntStack { - - private int[] ar; - private int pos = 0, sz; - - // max_sz is the maximum number of items - // that can be in the queue at any given time - public IntStack(int max_sz) { - ar = new int[(sz = max_sz)]; - } - - public boolean isEmpty() { - return pos == 0; - } - - // Returns the element at the top of the stack - public int peek() { - return ar[pos - 1]; - } - - // Add an element to the top of the stack - public void push(int value) { - ar[pos++] = value; - } - - // Make sure you check that the stack is not empty before calling pop! - public int pop() { - return ar[--pos]; - } -} - -public class DepthFirstSearchAdjacencyListIterativeFastStack { - - static class Edge { - int from, to, cost; - - public Edge(int from, int to, int cost) { - this.from = from; - this.to = to; - this.cost = cost; - } - } - - // Perform a depth first search on a graph with n nodes - // from a starting point to count the number of nodes - // in a given component. - static int dfs(Map> graph, int start, int n) { - - int count = 0; - boolean[] visited = new boolean[n]; - IntStack stack = new IntStack(n); - - // Start by visiting the starting node - stack.push(start); - - while (!stack.isEmpty()) { - int node = stack.pop(); - if (!visited[node]) { - - count++; - visited[node] = true; - List edges = graph.get(node); - - if (edges != null) { - for (Edge edge : edges) { - if (!visited[edge.to]) { - stack.push(edge.to); - } - } - } - } - } - - return count; - } - - // Example usage of DFS - public static void main(String[] args) { - - // Create a fully connected graph - // (0) - // / \ - // 5 / \ 4 - // / \ - // 10 < -2 > - // +->(2)<------(1) (4) - // +--- \ / - // \ / - // 1 \ / 6 - // > < - // (3) - int numNodes = 5; - Map> graph = new HashMap<>(); - addDirectedEdge(graph, 0, 1, 4); - addDirectedEdge(graph, 0, 2, 5); - addDirectedEdge(graph, 1, 2, -2); - addDirectedEdge(graph, 1, 3, 6); - addDirectedEdge(graph, 2, 3, 1); - addDirectedEdge(graph, 2, 2, 10); // Self loop - - long nodeCount = dfs(graph, 0, numNodes); - System.out.println("DFS node count starting at node 0: " + nodeCount); - if (nodeCount != 4) System.err.println("Error with DFS"); - - nodeCount = dfs(graph, 4, numNodes); - System.out.println("DFS node count starting at node 4: " + nodeCount); - if (nodeCount != 1) System.err.println("Error with DFS"); - } - - // Helper method to setup graph - private static void addDirectedEdge(Map> graph, int from, int to, int cost) { - List list = graph.get(from); - if (list == null) { - list = new ArrayList(); - graph.put(from, list); - } - list.add(new Edge(from, to, cost)); - } -} diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/DepthFirstSearchAdjacencyListRecursive.java b/src/main/java/com/williamfiset/algorithms/graphtheory/DepthFirstSearchAdjacencyListRecursive.java index 4c50679d0..6a91fbd9b 100644 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/DepthFirstSearchAdjacencyListRecursive.java +++ b/src/main/java/com/williamfiset/algorithms/graphtheory/DepthFirstSearchAdjacencyListRecursive.java @@ -1,86 +1,74 @@ /** - * An implementation of a recursive approach to DFS Time Complexity: O(V + E) + * Depth-First Search — Adjacency List (Recursive) + * + *

Performs a recursive DFS traversal on a directed graph represented as an + * adjacency list, counting the number of reachable nodes from a given source. + * + *

Time: O(V + E) + *

Space: O(V) * * @author William Fiset, william.alexandre.fiset@gmail.com */ package com.williamfiset.algorithms.graphtheory; -import java.util.*; +import java.util.ArrayList; +import java.util.List; public class DepthFirstSearchAdjacencyListRecursive { - static class Edge { - int from, to, cost; - - public Edge(int from, int to, int cost) { - this.from = from; - this.to = to; - this.cost = cost; - } + /** + * Returns the number of nodes reachable from {@code start} (including itself). + */ + static int dfs(int start, List> graph) { + return dfs(start, new boolean[graph.size()], graph); } - // Perform a depth first search on the graph counting - // the number of nodes traversed starting at some position - static long dfs(int at, boolean[] visited, Map> graph) { - - // We have already visited this node - if (visited[at]) return 0L; - - // Visit this node + private static int dfs(int at, boolean[] visited, List> graph) { + if (visited[at]) { + return 0; + } visited[at] = true; - long count = 1; - - // Visit all edges adjacent to where we're at - List edges = graph.get(at); - if (edges != null) { - for (Edge edge : edges) { - count += dfs(edge.to, visited, graph); - } + int count = 1; + for (int to : graph.get(at)) { + count += dfs(to, visited, graph); } - return count; } - // Example usage of DFS - public static void main(String[] args) { + // ==================== Main ==================== - // Create a fully connected graph - // (0) - // / \ - // 5 / \ 4 - // / \ - // 10 < -2 > - // +->(2)<------(1) (4) - // +--- \ / - // \ / - // 1 \ / 6 - // > < - // (3) - int numNodes = 5; - Map> graph = new HashMap<>(); - addDirectedEdge(graph, 0, 1, 4); - addDirectedEdge(graph, 0, 2, 5); - addDirectedEdge(graph, 1, 2, -2); - addDirectedEdge(graph, 1, 3, 6); - addDirectedEdge(graph, 2, 3, 1); - addDirectedEdge(graph, 2, 2, 10); // Self loop + // + // 0 ---> 1 ---> 3 + // | | + // v v + // 2 4 ---> 5 6 + // + // DFS from 0: 6 nodes + // DFS from 6: 1 node + // + public static void main(String[] args) { + int n = 7; + List> graph = createGraph(n); - long nodeCount = dfs(0, new boolean[numNodes], graph); - System.out.println("DFS node count starting at node 0: " + nodeCount); - if (nodeCount != 4) System.err.println("Error with DFS"); + addDirectedEdge(graph, 0, 1); + addDirectedEdge(graph, 0, 2); + addDirectedEdge(graph, 1, 3); + addDirectedEdge(graph, 1, 4); + addDirectedEdge(graph, 4, 5); - nodeCount = dfs(4, new boolean[numNodes], graph); - System.out.println("DFS node count starting at node 4: " + nodeCount); - if (nodeCount != 1) System.err.println("Error with DFS"); + System.out.println("DFS from 0: " + dfs(0, graph) + " nodes"); + System.out.println("DFS from 6: " + dfs(6, graph) + " nodes"); } - // Helper method to setup graph - private static void addDirectedEdge(Map> graph, int from, int to, int cost) { - List list = graph.get(from); - if (list == null) { - list = new ArrayList(); - graph.put(from, list); + private static List> createGraph(int n) { + List> graph = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + graph.add(new ArrayList<>()); } - list.add(new Edge(from, to, cost)); + return graph; + } + + private static void addDirectedEdge(List> graph, int from, int to) { + graph.get(from).add(to); } } diff --git a/src/test/java/com/williamfiset/algorithms/graphtheory/BUILD b/src/test/java/com/williamfiset/algorithms/graphtheory/BUILD index 6a610086b..930c75d75 100644 --- a/src/test/java/com/williamfiset/algorithms/graphtheory/BUILD +++ b/src/test/java/com/williamfiset/algorithms/graphtheory/BUILD @@ -24,6 +24,17 @@ TEST_DEPS = [ # Core graphtheory tests +# bazel test //src/test/java/com/williamfiset/algorithms/graphtheory:ConnectedComponentsUnionFindTest +java_test( + name = "ConnectedComponentsUnionFindTest", + srcs = ["ConnectedComponentsUnionFindTest.java"], + main_class = "org.junit.platform.console.ConsoleLauncher", + use_testrunner = False, + args = ["--select-class=com.williamfiset.algorithms.graphtheory.ConnectedComponentsUnionFindTest"], + runtime_deps = JUNIT5_RUNTIME_DEPS, + deps = TEST_DEPS, +) + # bazel test //src/test/java/com/williamfiset/algorithms/graphtheory:BoruvkasTest java_test( name = "BoruvkasTest", diff --git a/src/test/java/com/williamfiset/algorithms/graphtheory/BoruvkasTest.java b/src/test/java/com/williamfiset/algorithms/graphtheory/BoruvkasTest.java index 4d0585c65..6c084c501 100644 --- a/src/test/java/com/williamfiset/algorithms/graphtheory/BoruvkasTest.java +++ b/src/test/java/com/williamfiset/algorithms/graphtheory/BoruvkasTest.java @@ -18,24 +18,24 @@ public void testNullGraphThrowsException() { public void testSingleNode() { Edge[] graph = new Edge[0]; Boruvkas solver = new Boruvkas(1, graph); - assertThat(solver.getMstCost()).isEqualTo(0L); - assertThat(solver.getMst()).isEmpty(); + assertThat(solver.getMstCost().getAsLong()).isEqualTo(0L); + assertThat(solver.getMst().get()).isEmpty(); } @Test public void testTwoNodesConnected() { Edge[] graph = new Edge[] {new Edge(0, 1, 5)}; Boruvkas solver = new Boruvkas(2, graph); - assertThat(solver.getMstCost()).isEqualTo(5L); - assertThat(solver.getMst()).hasSize(1); + assertThat(solver.getMstCost().getAsLong()).isEqualTo(5L); + assertThat(solver.getMst().get()).hasSize(1); } @Test public void testTwoNodesDisconnected() { Edge[] graph = new Edge[0]; Boruvkas solver = new Boruvkas(2, graph); - assertThat(solver.getMstCost()).isNull(); - assertThat(solver.getMst()).isNull(); + assertThat(solver.getMstCost().isEmpty()).isTrue(); + assertThat(solver.getMst().isEmpty()).isTrue(); } @Test @@ -43,8 +43,8 @@ public void testSimpleTriangle() { Edge[] graph = new Edge[] {new Edge(0, 1, 1), new Edge(1, 2, 2), new Edge(0, 2, 3)}; Boruvkas solver = new Boruvkas(3, graph); - assertThat(solver.getMstCost()).isEqualTo(3L); - assertThat(solver.getMst()).hasSize(2); + assertThat(solver.getMstCost().getAsLong()).isEqualTo(3L); + assertThat(solver.getMst().get()).hasSize(2); } @Test @@ -52,38 +52,29 @@ public void testDisconnectedGraph() { // Two separate components: {0,1} and {2,3} Edge[] graph = new Edge[] {new Edge(0, 1, 1), new Edge(2, 3, 2)}; Boruvkas solver = new Boruvkas(4, graph); - assertThat(solver.getMstCost()).isNull(); - assertThat(solver.getMst()).isNull(); + assertThat(solver.getMstCost().isEmpty()).isTrue(); + assertThat(solver.getMst().isEmpty()).isTrue(); } @Test public void testExampleFromMainMethod() { - int n = 10, m = 18, i = 0; - Edge[] g = new Edge[m]; + Edge[] g = { + new Edge(0, 1, 1), + new Edge(1, 2, 7), + new Edge(2, 3, 2), + new Edge(0, 4, 4), + new Edge(1, 5, 3), + new Edge(2, 6, 5), + new Edge(3, 7, 6), + new Edge(4, 5, 8), + new Edge(5, 6, 2), + new Edge(6, 7, 9), + }; - g[i++] = new Edge(0, 1, 5); - g[i++] = new Edge(0, 3, 4); - g[i++] = new Edge(0, 4, 1); - g[i++] = new Edge(1, 2, 4); - g[i++] = new Edge(1, 3, 2); - g[i++] = new Edge(2, 7, 4); - g[i++] = new Edge(2, 8, 1); - g[i++] = new Edge(2, 9, 2); - g[i++] = new Edge(3, 6, 11); - g[i++] = new Edge(3, 7, 2); - g[i++] = new Edge(4, 3, 2); - g[i++] = new Edge(4, 5, 1); - g[i++] = new Edge(5, 3, 5); - g[i++] = new Edge(5, 6, 7); - g[i++] = new Edge(6, 7, 1); - g[i++] = new Edge(6, 8, 4); - g[i++] = new Edge(7, 8, 6); - g[i++] = new Edge(9, 8, 0); + Boruvkas solver = new Boruvkas(8, g); - Boruvkas solver = new Boruvkas(n, g); - - assertThat(solver.getMstCost()).isEqualTo(14L); - assertThat(solver.getMst()).hasSize(n - 1); + assertThat(solver.getMstCost().getAsLong()).isEqualTo(23L); + assertThat(solver.getMst().get()).hasSize(7); } @Test @@ -94,8 +85,8 @@ public void testLinearGraph() { new Edge(0, 1, 1), new Edge(1, 2, 2), new Edge(2, 3, 3), new Edge(3, 4, 4) }; Boruvkas solver = new Boruvkas(5, graph); - assertThat(solver.getMstCost()).isEqualTo(10L); - assertThat(solver.getMst()).hasSize(4); + assertThat(solver.getMstCost().getAsLong()).isEqualTo(10L); + assertThat(solver.getMst().get()).hasSize(4); } @Test @@ -112,8 +103,8 @@ public void testCompleteGraphK4() { }; Boruvkas solver = new Boruvkas(4, graph); // MST should be: 0-1 (1), 1-2 (2), 0-3 (3) = 6 - assertThat(solver.getMstCost()).isEqualTo(6L); - assertThat(solver.getMst()).hasSize(3); + assertThat(solver.getMstCost().getAsLong()).isEqualTo(6L); + assertThat(solver.getMst().get()).hasSize(3); } @Test @@ -121,8 +112,8 @@ public void testGraphWithZeroWeightEdges() { Edge[] graph = new Edge[] {new Edge(0, 1, 0), new Edge(1, 2, 0), new Edge(2, 3, 0)}; Boruvkas solver = new Boruvkas(4, graph); - assertThat(solver.getMstCost()).isEqualTo(0L); - assertThat(solver.getMst()).hasSize(3); + assertThat(solver.getMstCost().getAsLong()).isEqualTo(0L); + assertThat(solver.getMst().get()).hasSize(3); } @Test @@ -130,8 +121,8 @@ public void testGraphWithNegativeWeightEdges() { Edge[] graph = new Edge[] {new Edge(0, 1, -5), new Edge(1, 2, -3), new Edge(0, 2, 10)}; Boruvkas solver = new Boruvkas(3, graph); - assertThat(solver.getMstCost()).isEqualTo(-8L); - assertThat(solver.getMst()).hasSize(2); + assertThat(solver.getMstCost().getAsLong()).isEqualTo(-8L); + assertThat(solver.getMst().get()).hasSize(2); } @Test @@ -146,8 +137,8 @@ public void testGraphWithEqualWeightEdges() { new Edge(0, 2, 5) }; Boruvkas solver = new Boruvkas(4, graph); - assertThat(solver.getMstCost()).isEqualTo(15L); - assertThat(solver.getMst()).hasSize(3); + assertThat(solver.getMstCost().getAsLong()).isEqualTo(15L); + assertThat(solver.getMst().get()).hasSize(3); } @Test @@ -158,8 +149,8 @@ public void testStarGraph() { new Edge(0, 1, 1), new Edge(0, 2, 2), new Edge(0, 3, 3), new Edge(0, 4, 4) }; Boruvkas solver = new Boruvkas(5, graph); - assertThat(solver.getMstCost()).isEqualTo(10L); - assertThat(solver.getMst()).hasSize(4); + assertThat(solver.getMstCost().getAsLong()).isEqualTo(10L); + assertThat(solver.getMst().get()).hasSize(4); } @Test @@ -169,10 +160,10 @@ public void testMstIsIdempotent() { Boruvkas solver = new Boruvkas(3, graph); // Call multiple times to verify idempotency - Long cost1 = solver.getMstCost(); - Long cost2 = solver.getMstCost(); - List mst1 = solver.getMst(); - List mst2 = solver.getMst(); + long cost1 = solver.getMstCost().getAsLong(); + long cost2 = solver.getMstCost().getAsLong(); + List mst1 = solver.getMst().get(); + List mst2 = solver.getMst().get(); assertThat(cost1).isEqualTo(cost2); assertThat(mst1).isEqualTo(mst2); @@ -200,8 +191,8 @@ public void testLargerGraph() { }; Boruvkas solver = new Boruvkas(9, graph); // Known MST cost for this classic graph - assertThat(solver.getMstCost()).isEqualTo(37L); - assertThat(solver.getMst()).hasSize(8); + assertThat(solver.getMstCost().getAsLong()).isEqualTo(37L); + assertThat(solver.getMst().get()).hasSize(8); } @Test @@ -215,9 +206,8 @@ public void testMstEdgesFormSpanningTree() { new Edge(0, 2, 5) }; Boruvkas solver = new Boruvkas(4, graph); - List mst = solver.getMst(); + List mst = solver.getMst().get(); - assertThat(mst).isNotNull(); assertThat(mst).hasSize(3); // Verify MST connects all nodes (using simple connectivity check) @@ -253,7 +243,7 @@ public void testParallelEdges() { }; Boruvkas solver = new Boruvkas(3, graph); // Should pick the minimum weight edge between 0 and 1 - assertThat(solver.getMstCost()).isEqualTo(5L); - assertThat(solver.getMst()).hasSize(2); + assertThat(solver.getMstCost().getAsLong()).isEqualTo(5L); + assertThat(solver.getMst().get()).hasSize(2); } } diff --git a/src/test/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsUnionFindTest.java b/src/test/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsUnionFindTest.java new file mode 100644 index 000000000..cacf5f50b --- /dev/null +++ b/src/test/java/com/williamfiset/algorithms/graphtheory/ConnectedComponentsUnionFindTest.java @@ -0,0 +1,170 @@ +package com.williamfiset.algorithms.graphtheory; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.*; +import org.junit.jupiter.api.*; + +public class ConnectedComponentsUnionFindTest { + + private static List> createGraph(int n) { + List> graph = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + graph.add(new ArrayList<>()); + } + return graph; + } + + private static void addEdge(List> graph, int u, int v) { + graph.get(u).add(v); + graph.get(v).add(u); + } + + @Test + public void testNullGraphThrows() { + assertThrows(IllegalArgumentException.class, () -> new ConnectedComponentsUnionFind(null)); + } + + @Test + public void testEmptyGraph() { + List> graph = createGraph(0); + ConnectedComponentsUnionFind solver = new ConnectedComponentsUnionFind(graph); + assertThat(solver.countComponents()).isEqualTo(0); + } + + @Test + public void testSingleNode() { + List> graph = createGraph(1); + ConnectedComponentsUnionFind solver = new ConnectedComponentsUnionFind(graph); + assertThat(solver.countComponents()).isEqualTo(1); + assertThat(solver.componentSize(0)).isEqualTo(1); + } + + @Test + public void testTwoNodesConnected() { + List> graph = createGraph(2); + addEdge(graph, 0, 1); + ConnectedComponentsUnionFind solver = new ConnectedComponentsUnionFind(graph); + assertThat(solver.countComponents()).isEqualTo(1); + assertThat(solver.componentId(0)).isEqualTo(solver.componentId(1)); + assertThat(solver.componentSize(0)).isEqualTo(2); + assertThat(solver.componentSize(1)).isEqualTo(2); + } + + @Test + public void testTwoNodesDisconnected() { + List> graph = createGraph(2); + ConnectedComponentsUnionFind solver = new ConnectedComponentsUnionFind(graph); + assertThat(solver.countComponents()).isEqualTo(2); + assertThat(solver.componentId(0)).isNotEqualTo(solver.componentId(1)); + assertThat(solver.componentSize(0)).isEqualTo(1); + assertThat(solver.componentSize(1)).isEqualTo(1); + } + + @Test + public void testAllIsolatedNodes() { + int n = 5; + List> graph = createGraph(n); + ConnectedComponentsUnionFind solver = new ConnectedComponentsUnionFind(graph); + assertThat(solver.countComponents()).isEqualTo(5); + for (int i = 0; i < n; i++) { + assertThat(solver.componentSize(i)).isEqualTo(1); + } + } + + @Test + public void testFullyConnectedGraph() { + int n = 4; + List> graph = createGraph(n); + addEdge(graph, 0, 1); + addEdge(graph, 0, 2); + addEdge(graph, 0, 3); + addEdge(graph, 1, 2); + addEdge(graph, 1, 3); + addEdge(graph, 2, 3); + ConnectedComponentsUnionFind solver = new ConnectedComponentsUnionFind(graph); + assertThat(solver.countComponents()).isEqualTo(1); + for (int i = 0; i < n; i++) { + assertThat(solver.componentSize(i)).isEqualTo(4); + } + } + + @Test + public void testExampleFromMainMethod() { + // {0,1,2}, {3,4,6,9}, {5}, {7,8}, {10} + int n = 11; + List> graph = createGraph(n); + addEdge(graph, 0, 1); + addEdge(graph, 0, 2); + addEdge(graph, 1, 2); + addEdge(graph, 3, 4); + addEdge(graph, 3, 6); + addEdge(graph, 6, 9); + addEdge(graph, 7, 8); + + ConnectedComponentsUnionFind solver = new ConnectedComponentsUnionFind(graph); + assertThat(solver.countComponents()).isEqualTo(5); + + // Nodes in the same component share the same id. + assertThat(solver.componentId(0)).isEqualTo(solver.componentId(1)); + assertThat(solver.componentId(0)).isEqualTo(solver.componentId(2)); + assertThat(solver.componentId(3)).isEqualTo(solver.componentId(4)); + assertThat(solver.componentId(3)).isEqualTo(solver.componentId(6)); + assertThat(solver.componentId(3)).isEqualTo(solver.componentId(9)); + assertThat(solver.componentId(7)).isEqualTo(solver.componentId(8)); + + // Nodes in different components have different ids. + Set roots = new HashSet<>(); + roots.add(solver.componentId(0)); + roots.add(solver.componentId(3)); + roots.add(solver.componentId(5)); + roots.add(solver.componentId(7)); + roots.add(solver.componentId(10)); + assertThat(roots).hasSize(5); + + // Component sizes. + assertThat(solver.componentSize(0)).isEqualTo(3); + assertThat(solver.componentSize(3)).isEqualTo(4); + assertThat(solver.componentSize(5)).isEqualTo(1); + assertThat(solver.componentSize(7)).isEqualTo(2); + assertThat(solver.componentSize(10)).isEqualTo(1); + } + + @Test + public void testLinearChain() { + // 0 - 1 - 2 - 3 - 4 + int n = 5; + List> graph = createGraph(n); + addEdge(graph, 0, 1); + addEdge(graph, 1, 2); + addEdge(graph, 2, 3); + addEdge(graph, 3, 4); + ConnectedComponentsUnionFind solver = new ConnectedComponentsUnionFind(graph); + assertThat(solver.countComponents()).isEqualTo(1); + for (int i = 0; i < n; i++) { + assertThat(solver.componentSize(i)).isEqualTo(5); + } + } + + @Test + public void testSelfLoop() { + List> graph = createGraph(2); + addEdge(graph, 0, 0); + ConnectedComponentsUnionFind solver = new ConnectedComponentsUnionFind(graph); + assertThat(solver.countComponents()).isEqualTo(2); + assertThat(solver.componentSize(0)).isEqualTo(1); + assertThat(solver.componentSize(1)).isEqualTo(1); + } + + @Test + public void testIdempotent() { + List> graph = createGraph(3); + addEdge(graph, 0, 1); + ConnectedComponentsUnionFind solver = new ConnectedComponentsUnionFind(graph); + assertThat(solver.countComponents()).isEqualTo(2); + assertThat(solver.countComponents()).isEqualTo(2); + assertThat(solver.componentId(0)).isEqualTo(solver.componentId(0)); + assertThat(solver.componentSize(0)).isEqualTo(solver.componentSize(0)); + } +}