From 7336ab43053bccfd84f00b26a308c920aec78549 Mon Sep 17 00:00:00 2001 From: William Fiset Date: Tue, 24 Mar 2026 09:25:02 -0700 Subject: [PATCH 1/3] Remove GraphDiameter and all associated references Co-Authored-By: Claude Opus 4.6 --- README.md | 1 - .../williamfiset/algorithms/graphtheory/BUILD | 7 - .../algorithms/graphtheory/GraphDiameter.java | 156 ------------------ 3 files changed, 164 deletions(-) delete mode 100644 src/main/java/com/williamfiset/algorithms/graphtheory/GraphDiameter.java 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/GraphDiameter.java b/src/main/java/com/williamfiset/algorithms/graphtheory/GraphDiameter.java deleted file mode 100644 index 78ea44d4d..000000000 --- a/src/main/java/com/williamfiset/algorithms/graphtheory/GraphDiameter.java +++ /dev/null @@ -1,156 +0,0 @@ -/** - * Given a graph as a adjacency list this file shows you how to find the diameter/radius of the - * graph. - * - *

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 visited = new HashMap<>(); - static ArrayDeque queue = new ArrayDeque<>(); - - // Compute the eccentricity from a given node. The eccentricity - // is the distance to the furthest node(s). - private static int eccentricity(int nodeID, Map> graph) { - - VISITED_TOKEN++; - - queue.offer(nodeID); - queue.offer(DEPTH_TOKEN); - visited.put(nodeID, VISITED_TOKEN); - - int depth = 0; - - // Do BFS to count the - while (true) { - - Integer id = queue.poll(); - - // If we encounter a depth token this means that we - // have finished the current frontier and are about - // to start the new layer (some of which may already - // be in the queue) or have reached the end. - if (id == DEPTH_TOKEN) { - - // No more nodes to process - if (queue.isEmpty()) break; - - // Add another DEPTH_TOKEN - queue.offer(DEPTH_TOKEN); - - // Increase the max depth for each DEPTH_TOKEN seen - depth++; - - } else { - - List edges = graph.get(id); - if (edges != null) { - for (Edge edge : edges) { - if (!VISITED_TOKEN.equals(visited.get(edge.to))) { - visited.put(edge.to, VISITED_TOKEN); - queue.offer(edge.to); - } - } - } - } - } - - return depth; - } - - // Compute the diameter of an arbitrary graph - // NOTE: The input graph should be undirected - public static int graphDiameter(Map> graph) { - - if (graph == null) return 0; - - int diameter = 0; - int radius = Integer.MAX_VALUE; - - // The diameter of a graph is the maximum of all the eccentricity values from all nodes. - // The radius on the other hand is the minimum of all the eccentricity values from all nodes. - for (Integer nodeID : graph.keySet()) { - int eccentricity = eccentricity(nodeID, graph); - diameter = Math.max(diameter, eccentricity); - radius = Math.min(radius, eccentricity); - } - - // return radius; - return diameter; - } - - // Example usage of how to compute the diameter of a graph - public static void main(String[] args) { - - Map> graph = createGraph(5); - addUndirectedEdge(graph, 4, 2); - addUndirectedEdge(graph, 2, 0); - addUndirectedEdge(graph, 0, 1); - addUndirectedEdge(graph, 1, 2); - addUndirectedEdge(graph, 1, 3); - - int diameter = graphDiameter(graph); - if (diameter != 3) System.out.println("Wrong diameter!"); - - // No edges - graph = createGraph(5); - diameter = graphDiameter(graph); - if (diameter != 0) System.out.println("Wrong diameter!"); - - graph = createGraph(8); - addUndirectedEdge(graph, 0, 5); - addUndirectedEdge(graph, 1, 5); - addUndirectedEdge(graph, 2, 5); - addUndirectedEdge(graph, 3, 5); - addUndirectedEdge(graph, 4, 5); - addUndirectedEdge(graph, 6, 5); - addUndirectedEdge(graph, 7, 5); - diameter = graphDiameter(graph); - if (diameter != 2) System.out.println("Wrong diameter!"); - - graph = createGraph(9); - addUndirectedEdge(graph, 0, 5); - addUndirectedEdge(graph, 1, 5); - addUndirectedEdge(graph, 2, 5); - addUndirectedEdge(graph, 3, 5); - addUndirectedEdge(graph, 4, 5); - addUndirectedEdge(graph, 6, 5); - addUndirectedEdge(graph, 7, 5); - addUndirectedEdge(graph, 3, 8); - diameter = graphDiameter(graph); - if (diameter != 3) System.out.println("Wrong diameter!"); - } - - private static Map> createGraph(int numNodes) { - Map> graph = new HashMap<>(); - for (int i = 0; i < numNodes; i++) graph.put(i, new ArrayList()); - return graph; - } - - private static void addUndirectedEdge(Map> graph, int from, int to) { - graph.get(from).add(new Edge(from, to)); - graph.get(to).add(new Edge(to, from)); - } -} From 5f53462ff9a48259e0e72a6eea9bacbc3b2dd5ba Mon Sep 17 00:00:00 2001 From: William Fiset Date: Tue, 24 Mar 2026 09:26:59 -0700 Subject: [PATCH 2/3] Refactor FloydWarshallSolver: clean up, add docs, simplify examples (#1306) Co-authored-by: Claude Opus 4.6 --- .../graphtheory/FloydWarshallSolver.java | 186 ++++++++---------- 1 file changed, 79 insertions(+), 107 deletions(-) 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. * - *

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 reconstructShortestPath(int start, int end) { solve(); List path = new ArrayList<>(); - if (dp[start][end] == POSITIVE_INFINITY) return path; + if (dp[start][end] == POSITIVE_INFINITY) + return path; int at = start; for (; at != end; at = next[at][end]) { - // Return null since there are an infinite number of shortest paths. - if (at == REACHES_NEGATIVE_CYCLE) return null; + if (at == REACHES_NEGATIVE_CYCLE) + return null; path.add(at); } - // Return null since there are an infinite number of shortest paths. - if (next[at][end] == REACHES_NEGATIVE_CYCLE) return null; + if (next[at][end] == REACHES_NEGATIVE_CYCLE) + return null; path.add(end); return path; } - /* Example usage. */ - - // Creates a graph with n nodes. The adjacency matrix is constructed - // such that the value of going from a node to itself is 0. + /** Creates an n x n adjacency matrix initialized with POSITIVE_INFINITY and zero diagonal. */ public static double[][] createGraph(int n) { double[][] matrix = new double[n][n]; for (int i = 0; i < n; i++) { - java.util.Arrays.fill(matrix[i], POSITIVE_INFINITY); + Arrays.fill(matrix[i], POSITIVE_INFINITY); matrix[i][i] = 0; } return matrix; } public static void main(String[] args) { - // Construct graph. - int n = 7; - double[][] m = createGraph(n); + exampleWithNegativeCycle(); + System.out.println(); + exampleSimpleGraph(); + } - // Add some edge values. - m[0][1] = 2; - m[0][2] = 5; - m[0][6] = 10; - m[1][2] = 2; - m[1][4] = 11; - m[2][6] = 2; - m[6][5] = 11; - m[4][5] = 1; - m[5][4] = -2; + // Example 1: 4-node graph with a negative cycle between nodes 2 and 3. + private static void exampleWithNegativeCycle() { + int n = 4; + double[][] m = createGraph(n); + m[0][1] = 4; + m[1][2] = 1; + m[2][3] = 2; + m[3][2] = -5; // Creates negative cycle: 2 -> 3 -> 2 (net cost -3). FloydWarshallSolver solver = new FloydWarshallSolver(m); double[][] dist = solver.getApspMatrix(); - for (int i = 0; i < n; i++) - for (int j = 0; j < n; j++) - System.out.printf("This shortest path from node %d to node %d is %.3f\n", i, j, dist[i][j]); - - // Prints: - // This shortest path from node 0 to node 0 is 0.000 - // This shortest path from node 0 to node 1 is 2.000 - // This shortest path from node 0 to node 2 is 4.000 - // This shortest path from node 0 to node 3 is Infinity - // This shortest path from node 0 to node 4 is -Infinity - // This shortest path from node 0 to node 5 is -Infinity - // This shortest path from node 0 to node 6 is 6.000 - // This shortest path from node 1 to node 0 is Infinity - // This shortest path from node 1 to node 1 is 0.000 - // This shortest path from node 1 to node 2 is 2.000 - // This shortest path from node 1 to node 3 is Infinity - // ... + System.out.println("=== Example 1: Negative cycle ==="); + System.out.printf("dist(0, 1) = %.0f\n", dist[0][1]); // 4 + System.out.printf("dist(0, 2) = %.0f\n", dist[0][2]); // -Infinity (reaches negative cycle) + System.out.printf("path(0, 2) = %s\n", formatPath(solver.reconstructShortestPath(0, 2), 0, 2)); + System.out.printf("path(0, 1) = %s\n", formatPath(solver.reconstructShortestPath(0, 1), 0, 1)); + } - System.out.println(); + // Example 2: 4-node directed graph with no negative cycles. + private static void exampleSimpleGraph() { + int n = 4; + double[][] m = createGraph(n); + m[0][1] = 1; + m[1][2] = 3; + m[1][3] = 10; + m[2][3] = 2; - // Reconstructs the shortest paths from all nodes to every other nodes. - for (int i = 0; i < n; i++) { - for (int j = 0; j < n; j++) { - List path = solver.reconstructShortestPath(i, j); - String str; - if (path == null) { - str = "HAS AN ∞ NUMBER OF SOLUTIONS! (negative cycle case)"; - } else if (path.size() == 0) { - str = String.format("DOES NOT EXIST (node %d doesn't reach node %d)", i, j); - } else { - str = - String.join( - " -> ", - path.stream() - .map(Object::toString) - .collect(java.util.stream.Collectors.toList())); - str = "is: [" + str + "]"; - } - - System.out.printf("The shortest path from node %d to node %d %s\n", i, j, str); - } - } + FloydWarshallSolver solver = new FloydWarshallSolver(m); + double[][] dist = solver.getApspMatrix(); + + System.out.println("=== Example 2: Simple directed graph ==="); + // Shortest distance from 0 to 3 is 6 (0 -> 1 -> 2 -> 3), not 11 (0 -> 1 -> 3). + System.out.printf("dist(0, 3) = %.0f\n", dist[0][3]); + System.out.printf("path(0, 3) = %s\n", formatPath(solver.reconstructShortestPath(0, 3), 0, 3)); + System.out.printf("path(3, 0) = %s\n", formatPath(solver.reconstructShortestPath(3, 0), 3, 0)); + } - // Prints: - // The shortest path from node 0 to node 0 is: [0] - // The shortest path from node 0 to node 1 is: [0 -> 1] - // The shortest path from node 0 to node 2 is: [0 -> 1 -> 2] - // The shortest path from node 0 to node 3 DOES NOT EXIST (node 0 doesn't reach node 3) - // The shortest path from node 0 to node 4 HAS AN ∞ NUMBER OF SOLUTIONS! (negative cycle case) - // The shortest path from node 0 to node 5 HAS AN ∞ NUMBER OF SOLUTIONS! (negative cycle case) - // The shortest path from node 0 to node 6 is: [0 -> 1 -> 2 -> 6] - // The shortest path from node 1 to node 0 DOES NOT EXIST (node 1 doesn't reach node 0) - // The shortest path from node 1 to node 1 is: [1] - // The shortest path from node 1 to node 2 is: [1 -> 2] - // The shortest path from node 1 to node 3 DOES NOT EXIST (node 1 doesn't reach node 3) - // The shortest path from node 1 to node 4 HAS AN ∞ NUMBER OF SOLUTIONS! (negative cycle case) - // The shortest path from node 1 to node 5 HAS AN ∞ NUMBER OF SOLUTIONS! (negative cycle case) - // The shortest path from node 1 to node 6 is: [1 -> 2 -> 6] - // The shortest path from node 2 to node 0 DOES NOT EXIST (node 2 doesn't reach node 0) - // ... + private static String formatPath(List path, int start, int end) { + if (path == null) + return "NEGATIVE CYCLE"; + if (path.isEmpty()) + return String.format("NO PATH (%d doesn't reach %d)", start, end); + return path.stream().map(Object::toString).collect(Collectors.joining(" -> ")); } } From 96ec0eb979ce4978e3c5e01d7e19bb654940c058 Mon Sep 17 00:00:00 2001 From: William Fiset Date: Tue, 24 Mar 2026 09:30:39 -0700 Subject: [PATCH 3/3] Refactor EulerianPathDirectedEdgesAdjacencyList: add docs, clean up code (#1303) - Add comprehensive Javadoc with complexity and use cases - Rename graph to adj for consistency - Add educational comments explaining Hierholzer's algorithm - Clean up example cases and move main method to bottom - Ensure O(n + m) time and space complexity notation Co-authored-by: Claude Opus 4.6 --- ...ulerianPathDirectedEdgesAdjacencyList.java | 163 +++++++++++++----- 1 file changed, 118 insertions(+), 45 deletions(-) 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 path; - private List> 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 path; // Stores the final Eulerian path + private List> 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. + * + *

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> 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)); } }