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: + *
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 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 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 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 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> 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.
- *
- *
> 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)
+ *
+ *
> 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) {
+ 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) {
+ if (visited[at]) {
+ return 0;
+ }
visited[at] = true;
- long count = 1;
-
- // Visit all edges adjacent to where we're at
- 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
> 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
> 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
> 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));
+ }
+}