diff --git a/README.md b/README.md index 21f337945..95482aec3 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,6 @@ $ java -cp classes com.williamfiset.algorithms.search.BinarySearch - [: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))** - [:movie_camera:](https://www.youtube.com/watch?v=8MpoO2zA2l4) [Eulerian Path (directed edges)](src/main/java/com/williamfiset/algorithms/graphtheory/EulerianPathDirectedEdgesAdjacencyList.java) **- O(E+V)** - [:movie_camera:](https://www.youtube.com/watch?v=4NQ3HnhyNfQ) [Floyd Warshall algorithm (adjacency matrix, negative cycle check)](src/main/java/com/williamfiset/algorithms/graphtheory/FloydWarshallSolver.java) **- O(V3)** -- [Graph diameter (adjacency list)](src/main/java/com/williamfiset/algorithms/graphtheory/GraphDiameter.java) **- O(VE)** - [:movie_camera:](https://www.youtube.com/watch?v=cIBFEhD77b4) [Kahn's algorithm (topological sort, adjacency list)](src/main/java/com/williamfiset/algorithms/graphtheory/Kahns.java) **- O(E+V)** - [Kruskal's min spanning tree algorithm (edge list, union find)](src/main/java/com/williamfiset/algorithms/graphtheory/KruskalsEdgeList.java) **- O(Elog(E))** - [:movie_camera:](https://www.youtube.com/watch?v=JZBQLXgSGfs) [Kruskal's min spanning tree algorithm (edge list, union find, lazy sorting)](src/main/java/com/williamfiset/algorithms/graphtheory/KruskalsEdgeListPartialSortSolver.java) **- O(Elog(E))** diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/BUILD b/src/main/java/com/williamfiset/algorithms/graphtheory/BUILD index cca226819..4a5e0ef1f 100644 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/BUILD +++ b/src/main/java/com/williamfiset/algorithms/graphtheory/BUILD @@ -139,13 +139,6 @@ java_binary( runtime_deps = [":graphtheory"], ) -# bazel run //src/main/java/com/williamfiset/algorithms/graphtheory:GraphDiameter -java_binary( - name = "GraphDiameter", - main_class = "com.williamfiset.algorithms.graphtheory.GraphDiameter", - runtime_deps = [":graphtheory"], -) - # bazel run //src/main/java/com/williamfiset/algorithms/graphtheory:Kahns java_binary( name = "Kahns", diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/EulerianPathDirectedEdgesAdjacencyList.java b/src/main/java/com/williamfiset/algorithms/graphtheory/EulerianPathDirectedEdgesAdjacencyList.java index a919bf961..1a19861b4 100644 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/EulerianPathDirectedEdgesAdjacencyList.java +++ b/src/main/java/com/williamfiset/algorithms/graphtheory/EulerianPathDirectedEdgesAdjacencyList.java @@ -1,14 +1,23 @@ /** - * Implementation of finding an Eulerian Path on a graph. This implementation verifies that the - * input graph is fully connected and supports self loops and repeated edges between nodes. + * Implementation of finding an Eulerian Path on a directed graph. This implementation verifies that + * the input graph is fully connected (all edges are reachable) and supports self loops and repeated + * edges between nodes. * - *
Test against: https://open.kattis.com/problems/eulerianpath - * http://codeforces.com/contest/508/problem/D + *
An Eulerian Path is a path in a graph that visits every edge exactly once. An Eulerian Circuit + * is an Eulerian Path which starts and ends on the same vertex. * - *
Run: bazel run //src/main/java/com/williamfiset/algorithms/graphtheory:EulerianPathDirectedEdgesAdjacencyList + *
Test against: + *
Time Complexity: O(E) + *
Run with: + * bazel run //src/main/java/com/williamfiset/algorithms/graphtheory:EulerianPathDirectedEdgesAdjacencyList * + *
Time Complexity: O(V + E)
+ *
+ * @see Eulerian Path (Wikipedia)
* @author William Fiset, william.alexandre.fiset@gmail.com
*/
package com.williamfiset.algorithms.graphtheory;
@@ -20,47 +29,69 @@
public class EulerianPathDirectedEdgesAdjacencyList {
- private final int n;
- private int edgeCount;
- private int[] in, out;
- private LinkedList The algorithm first verifies the necessary conditions for an Eulerian path based on vertex
+ * degrees and then uses Hierholzer's algorithm to construct the path via DFS.
+ *
+ * @return An array of node IDs representing the Eulerian path, or null if no path exists or the
+ * graph is disconnected.
+ *
+ * Time: O(V + E)
+ * Space: O(V + E)
+ */
public int[] getEulerianPath() {
setUp();
- if (!graphHasEulerianPath()) return null;
+ if (!graphHasEulerianPath()) {
+ return null;
+ }
+
+ // Start DFS from a valid starting node
dfs(findStartNode());
- // Make sure all edges of the graph were traversed. It could be the
- // case that the graph is disconnected in which case return null.
- if (path.size() != edgeCount + 1) return null;
+ // Check if all edges were traversed. If the graph is disconnected
+ // (excluding isolated nodes with no edges), path.size() will be less than edgeCount + 1.
+ if (path.size() != edgeCount + 1) {
+ return null;
+ }
- // Instead of returning the 'path' as a linked list return
- // the solution as a primitive array for convenience.
+ // Convert the path from LinkedList to a primitive array for the caller's convenience.
int[] soln = new int[edgeCount + 1];
- for (int i = 0; !path.isEmpty(); i++) soln[i] = path.removeFirst();
+ for (int i = 0; !path.isEmpty(); i++) {
+ soln[i] = path.removeFirst();
+ }
return soln;
}
+ // Pre-computes in-degrees, out-degrees and the total edge count.
private void setUp() {
- // Arrays that track the in degree and out degree of each node.
in = new int[n];
out = new int[n];
-
edgeCount = 0;
- // Compute in and out node degrees.
for (int from = 0; from < n; from++) {
for (int to : graph.get(from)) {
in[to]++;
@@ -70,45 +101,87 @@ private void setUp() {
}
}
+ // A directed graph has an Eulerian path if and only if:
+ // 1. At most one vertex has outDegree - inDegree = 1 (start node)
+ // 2. At most one vertex has inDegree - outDegree = 1 (end node)
+ // 3. All other vertices have inDegree == outDegree
private boolean graphHasEulerianPath() {
- if (edgeCount == 0) return false;
+ if (edgeCount == 0) {
+ return false;
+ }
int startNodes = 0, endNodes = 0;
for (int i = 0; i < n; i++) {
- if (out[i] - in[i] > 1 || in[i] - out[i] > 1) return false;
- else if (out[i] - in[i] == 1) startNodes++;
- else if (in[i] - out[i] == 1) endNodes++;
+ int diff = out[i] - in[i];
+ if (Math.abs(diff) > 1) {
+ return false;
+ } else if (diff == 1) {
+ startNodes++;
+ } else if (diff == -1) {
+ endNodes++;
+ }
}
return (endNodes == 0 && startNodes == 0) || (endNodes == 1 && startNodes == 1);
}
+ // Identifies a node to begin the Eulerian path traversal.
private int findStartNode() {
int start = 0;
for (int i = 0; i < n; i++) {
- // Unique starting node.
- if (out[i] - in[i] == 1) return i;
- // Start at a node with an outgoing edge.
- if (out[i] > 0) start = i;
+ // If a node has one more outgoing edge than incoming, it MUST be the start.
+ if (out[i] - in[i] == 1) {
+ return i;
+ }
+ // Otherwise, start at the first node encountered with at least one outgoing edge.
+ if (out[i] > 0) {
+ start = i;
+ }
}
return start;
}
- // Perform DFS to find Eulerian path.
+ /**
+ * Recursive DFS implementation of Hierholzer's algorithm.
+ *
+ * We traverse edges until we reach a node with no remaining outgoing edges, then backtrack.
+ * During backtracking, we add the current node to the front of the path. This naturally merges
+ * all sub-cycles into the main path.
+ *
+ * @param at The current node in the DFS traversal
+ */
private void dfs(int at) {
while (out[at] != 0) {
+ // Pick the next available edge. We decrement out[at] to "remove" the edge
+ // and use it as an index to select the next neighbor. This is O(1) per edge.
int next = graph.get(at).get(--out[at]);
dfs(next);
}
+ // As we backtrack from the recursion, add nodes to the start of the path.
path.addFirst(at);
}
/* Graph creation helper methods */
+ /**
+ * Initializes an empty adjacency list with n nodes.
+ *
+ * @param n The number of nodes in the graph
+ * @return An empty adjacency list
+ */
public static List Time Complexity: O(V^3)
+ * Time: O(V^3)
+ *
+ * Space: O(V^2)
*
* @author Micah Stairs, William Fiset
*/
package com.williamfiset.algorithms.graphtheory;
-// Import Java's special constants ∞ and -∞ which behave
-// as you expect them to when you do arithmetic. For example,
-// ∞ + ∞ = ∞, ∞ + x = ∞, -∞ + x = -∞ and ∞ + -∞ = Nan
import static java.lang.Double.NEGATIVE_INFINITY;
import static java.lang.Double.POSITIVE_INFINITY;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.stream.Collectors;
public class FloydWarshallSolver {
- private int n;
+ private final int n;
private boolean solved;
private double[][] dp;
private Integer[][] next;
@@ -28,22 +28,27 @@ public class FloydWarshallSolver {
private static final int REACHES_NEGATIVE_CYCLE = -1;
/**
- * As input, this class takes an adjacency matrix with edge weights between nodes, where
- * POSITIVE_INFINITY is used to indicate that two nodes are not connected.
+ * Creates a Floyd-Warshall solver from an adjacency matrix with edge weights between nodes, where
+ * POSITIVE_INFINITY indicates that two nodes are not connected.
*
* NOTE: Usually the diagonal of the adjacency matrix is all zeros (i.e. matrix[i][i] = 0 for
- * all i) since there is typically no cost to go from a node to itself, but this may depend on
- * your graph and the problem you are trying to solve.
+ * all i) since there is typically no cost to go from a node to itself, but this may depend on the
+ * graph and the problem being solved.
+ *
+ * @param matrix an n x n adjacency matrix of edge weights.
+ * @throws IllegalArgumentException if the matrix is null or empty.
*/
public FloydWarshallSolver(double[][] matrix) {
+ if (matrix == null || matrix.length == 0)
+ throw new IllegalArgumentException("Matrix cannot be null or empty.");
n = matrix.length;
dp = new double[n][n];
next = new Integer[n][n];
- // Copy input matrix and setup 'next' matrix for path reconstruction.
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
- if (matrix[i][j] != POSITIVE_INFINITY) next[i][j] = j;
+ if (matrix[i][j] != POSITIVE_INFINITY)
+ next[i][j] = j;
dp[i][j] = matrix[i][j];
}
}
@@ -52,30 +57,28 @@ public FloydWarshallSolver(double[][] matrix) {
/**
* Runs Floyd-Warshall to compute the shortest distance between every pair of nodes.
*
- * @return The solved All Pairs Shortest Path (APSP) matrix.
+ * @return the solved All Pairs Shortest Path (APSP) matrix.
*/
public double[][] getApspMatrix() {
solve();
return dp;
}
- // Executes the Floyd-Warshall algorithm.
+ /** Executes the Floyd-Warshall algorithm. */
public void solve() {
- if (solved) return;
+ if (solved)
+ return;
// Compute all pairs shortest paths.
- for (int k = 0; k < n; k++) {
- for (int i = 0; i < n; i++) {
- for (int j = 0; j < n; j++) {
+ for (int k = 0; k < n; k++)
+ for (int i = 0; i < n; i++)
+ for (int j = 0; j < n; j++)
if (dp[i][k] + dp[k][j] < dp[i][j]) {
dp[i][j] = dp[i][k] + dp[k][j];
next[i][j] = next[i][k];
}
- }
- }
- }
- // Identify negative cycles by propagating the value 'NEGATIVE_INFINITY'
+ // Identify negative cycles by propagating NEGATIVE_INFINITY
// to every edge that is part of or reaches into a negative cycle.
for (int k = 0; k < n; k++)
for (int i = 0; i < n; i++)
@@ -91,117 +94,86 @@ public void solve() {
/**
* Reconstructs the shortest path (of nodes) from 'start' to 'end' inclusive.
*
- * @return An array of nodes indexes of the shortest path from 'start' to 'end'. If 'start' and
- * 'end' are not connected return an empty array. If the shortest path from 'start' to 'end'
- * are reachable by a negative cycle return -1.
+ * @return an array of node indexes of the shortest path from 'start' to 'end'. If 'start' and
+ * 'end' are not connected return an empty list. If the shortest path from 'start' to 'end'
+ * reaches a negative cycle return null.
*/
public List Time Complexity: O(V(V + E)) = O(V^2 + VE))= O(VE)
- *
- * NOTE: This file could use some tests.
- *
- * @author William Fiset, william.alexandre.fiset@gmail.com
- */
-package com.williamfiset.algorithms.graphtheory;
-
-import java.util.*;
-
-public class GraphDiameter {
-
- static class Edge {
- int from, to;
-
- public Edge(int from, int to) {
- this.from = from;
- this.to = to;
- }
- }
-
- // Separate each breadth first search layer with a DEPTH_TOKEN
- // to easily determine the distance to other nodes
- static final int DEPTH_TOKEN = -1;
-
- static Integer VISITED_TOKEN = 0;
- static Map> graph;
-
+ private final int n; // Number of nodes in the graph
+ private int edgeCount; // Number of edges in the graph
+ private int[] in, out; // Arrays to track in-degree and out-degree of each node
+ private LinkedList
> graph; // Adjacency list representation of the graph
+
+ /**
+ * Initializes the solver with an adjacency list representation of a directed graph.
+ *
+ * @param graph Adjacency list where graph.get(i) contains the neighbors of node i
+ */
public EulerianPathDirectedEdgesAdjacencyList(List
> graph) {
- if (graph == null) throw new IllegalArgumentException("Graph cannot be null");
- n = graph.size();
+ if (graph == null) {
+ throw new IllegalArgumentException("Graph cannot be null");
+ }
+ this.n = graph.size();
this.graph = graph;
- path = new LinkedList<>();
+ this.path = new LinkedList<>();
}
- // Returns a list of edgeCount + 1 node ids that give the Eulerian path or
- // null if no path exists or the graph is disconnected.
+ /**
+ * Finds an Eulerian path in the graph if one exists.
+ *
+ *
> initializeEmptyGraph(int n) {
List
> graph = new ArrayList<>(n);
- for (int i = 0; i < n; i++) graph.add(new ArrayList<>());
+ for (int i = 0; i < n; i++) {
+ graph.add(new ArrayList<>());
+ }
return graph;
}
+ /**
+ * Adds a directed edge from one node to another.
+ *
+ * @param g Adjacency list to add the edge to
+ * @param from The source node index
+ * @param to The destination node index
+ */
public static void addDirectedEdge(List
> g, int from, int to) {
g.get(from).add(to);
}
@@ -137,11 +210,11 @@ private static void exampleFromSlides() {
addDirectedEdge(graph, 5, 6);
addDirectedEdge(graph, 6, 3);
- EulerianPathDirectedEdgesAdjacencyList solver;
- solver = new EulerianPathDirectedEdgesAdjacencyList(graph);
+ EulerianPathDirectedEdgesAdjacencyList solver = new EulerianPathDirectedEdgesAdjacencyList(graph);
- // Outputs path: [1, 3, 5, 6, 3, 2, 4, 3, 1, 2, 2, 4, 6]
- System.out.println(Arrays.toString(solver.getEulerianPath()));
+ // Expected path: [1, 3, 5, 6, 3, 2, 4, 3, 1, 2, 2, 4, 6]
+ int[] path = solver.getEulerianPath();
+ System.out.println("Path from slides: " + Arrays.toString(path));
}
private static void smallExample() {
@@ -155,10 +228,10 @@ private static void smallExample() {
addDirectedEdge(graph, 2, 1);
addDirectedEdge(graph, 4, 1);
- EulerianPathDirectedEdgesAdjacencyList solver;
- solver = new EulerianPathDirectedEdgesAdjacencyList(graph);
+ EulerianPathDirectedEdgesAdjacencyList solver = new EulerianPathDirectedEdgesAdjacencyList(graph);
- // Outputs path: [0, 1, 4, 1, 2, 1, 3]
- System.out.println(Arrays.toString(solver.getEulerianPath()));
+ // Expected path: [0, 1, 4, 1, 2, 1, 3]
+ int[] path = solver.getEulerianPath();
+ System.out.println("Small example path: " + Arrays.toString(path));
}
}
diff --git a/src/main/java/com/williamfiset/algorithms/graphtheory/FloydWarshallSolver.java b/src/main/java/com/williamfiset/algorithms/graphtheory/FloydWarshallSolver.java
index 7bf794f18..73e87e550 100644
--- a/src/main/java/com/williamfiset/algorithms/graphtheory/FloydWarshallSolver.java
+++ b/src/main/java/com/williamfiset/algorithms/graphtheory/FloydWarshallSolver.java
@@ -1,26 +1,26 @@
/**
- * This file contains an implementation of the Floyd-Warshall algorithm to find all pairs of
- * shortest paths between nodes in a graph. We also demonstrate how to detect negative cycles and
- * reconstruct the shortest path.
+ * Implementation of the Floyd-Warshall algorithm to find all pairs of shortest paths between nodes
+ * in a graph. Also demonstrates how to detect negative cycles and reconstruct the shortest path.
*
- *