diff --git a/src/main/java/com/thealgorithms/graph/HierholzerEulerianPath.java b/src/main/java/com/thealgorithms/graph/HierholzerEulerianPath.java new file mode 100644 index 000000000000..82da5c8163e5 --- /dev/null +++ b/src/main/java/com/thealgorithms/graph/HierholzerEulerianPath.java @@ -0,0 +1,303 @@ +package com.thealgorithms.graph; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.List; + +/** + * Implementation of Hierholzer's Algorithm for finding an Eulerian Path or Circuit + * in a directed graph. + * + *

+ * An Eulerian Circuit is a path that starts and ends at the same vertex + * and visits every edge exactly once. + *

+ * + *

+ * An Eulerian Path visits every edge exactly once but may start and end + * at different vertices. + *

+ * + *

+ * Algorithm Summary:
+ * 1. Compute indegree and outdegree for all vertices.
+ * 2. Check if the graph satisfies Eulerian path or circuit conditions.
+ * 3. Verify that all vertices with non-zero degree are weakly connected (undirected connectivity).
+ * 4. Use Hierholzer’s algorithm to build the path by exploring unused edges iteratively. + *

+ * + *

+ * Time Complexity: O(E + V).
+ * Space Complexity: O(V + E). + *

+ * + * @author Wikipedia: Hierholzer algorithm + */ +public class HierholzerEulerianPath { + + /** + * Simple directed graph represented by adjacency lists. + */ + public static class Graph { + private final List> adjacencyList; + + /** + * Constructs a graph with a given number of vertices. + * + * @param numNodes number of vertices + */ + public Graph(int numNodes) { + adjacencyList = new ArrayList<>(); + for (int i = 0; i < numNodes; i++) { + adjacencyList.add(new ArrayList<>()); + } + } + + /** + * Adds a directed edge from vertex {@code from} to vertex {@code to}. + * + * @param from source vertex + * @param to destination vertex + */ + public void addEdge(int from, int to) { + adjacencyList.get(from).add(to); + } + + /** + * Returns a list of outgoing edges from the given vertex. + * + * @param node vertex index + * @return list of destination vertices + */ + public List getEdges(int node) { + return adjacencyList.get(node); + } + + /** + * Returns the number of vertices in the graph. + * + * @return number of vertices + */ + public int getNumNodes() { + return adjacencyList.size(); + } + } + + private final Graph graph; + + /** + * Creates a Hierholzer solver for the given graph. + * + * @param graph directed graph + */ + public HierholzerEulerianPath(Graph graph) { + this.graph = graph; + } + + /** + * Finds an Eulerian Path or Circuit using Hierholzer’s Algorithm. + * + * @return list of vertices representing the Eulerian Path/Circuit, + * or an empty list if none exists + */ + public List findEulerianPath() { + int n = graph.getNumNodes(); + + // empty graph -> no path + if (n == 0) { + return new ArrayList<>(); + } + + int[] inDegree = new int[n]; + int[] outDegree = new int[n]; + int edgeCount = computeDegrees(inDegree, outDegree); + + // no edges -> single vertex response requested by tests: [0] + if (edgeCount == 0) { + return Collections.singletonList(0); + } + + int startNode = determineStartNode(inDegree, outDegree); + if (startNode == -1) { + return new ArrayList<>(); + } + + if (!allNonZeroDegreeVerticesWeaklyConnected(startNode, n, outDegree, inDegree)) { + return new ArrayList<>(); + } + + List path = buildHierholzerPath(startNode, n); + if (path.size() != edgeCount + 1) { + return new ArrayList<>(); + } + + return rotateEulerianCircuitIfNeeded(path, outDegree, inDegree); + } + + private int computeDegrees(int[] inDegree, int[] outDegree) { + int edgeCount = 0; + for (int u = 0; u < graph.getNumNodes(); u++) { + for (int v : graph.getEdges(u)) { + outDegree[u]++; + inDegree[v]++; + edgeCount++; + } + } + return edgeCount; + } + + private int determineStartNode(int[] inDegree, int[] outDegree) { + int n = graph.getNumNodes(); + int startNode = -1; + int startCount = 0; + int endCount = 0; + + for (int i = 0; i < n; i++) { + int diff = outDegree[i] - inDegree[i]; + if (diff == 1) { + startNode = i; + startCount++; + } else if (diff == -1) { + endCount++; + } else if (Math.abs(diff) > 1) { + return -1; + } + } + + if (!((startCount == 1 && endCount == 1) || (startCount == 0 && endCount == 0))) { + return -1; + } + + if (startNode == -1) { + for (int i = 0; i < n; i++) { + if (outDegree[i] > 0) { + startNode = i; + break; + } + } + } + return startNode; + } + + private List buildHierholzerPath(int startNode, int n) { + List> tempAdj = new ArrayList<>(); + for (int i = 0; i < n; i++) { + tempAdj.add(new ArrayDeque<>(graph.getEdges(i))); + } + + Deque stack = new ArrayDeque<>(); + List path = new ArrayList<>(); + stack.push(startNode); + + while (!stack.isEmpty()) { + int u = stack.peek(); + if (!tempAdj.get(u).isEmpty()) { + stack.push(tempAdj.get(u).pollFirst()); + } else { + path.add(stack.pop()); + } + } + + Collections.reverse(path); + return path; + } + + private List rotateEulerianCircuitIfNeeded(List path, int[] outDegree, int[] inDegree) { + int startCount = 0; + int endCount = 0; + for (int i = 0; i < outDegree.length; i++) { + int diff = outDegree[i] - inDegree[i]; + if (diff == 1) { + startCount++; + } else if (diff == -1) { + endCount++; + } + } + + if (startCount == 0 && endCount == 0 && !path.isEmpty()) { + int preferredStart = -1; + for (int i = 0; i < outDegree.length; i++) { + if (outDegree[i] > 0) { + preferredStart = i; + break; + } + } + + if (preferredStart != -1 && path.get(0) != preferredStart) { + int idx = 0; + for (Integer node : path) { // replaced indexed loop + if (node == preferredStart) { + break; + } + idx++; + } + + if (idx > 0) { + List rotated = new ArrayList<>(); + int currentIndex = 0; + for (Integer node : path) { // replaced indexed loop + if (currentIndex >= idx) { + rotated.add(node); + } + currentIndex++; + } + currentIndex = 0; + for (Integer node : path) { // replaced indexed loop + if (currentIndex < idx) { + rotated.add(node); + } + currentIndex++; + } + path = rotated; + } + } + } + return path; + } + + /** + * Checks weak connectivity (undirected) among vertices that have non-zero degree. + * + * @param startNode node to start DFS from (must be a vertex with non-zero degree) + * @param n number of vertices + * @param outDegree out-degree array + * @param inDegree in-degree array + * @return true if all vertices having non-zero degree belong to a single weak component + */ + private boolean allNonZeroDegreeVerticesWeaklyConnected(int startNode, int n, int[] outDegree, int[] inDegree) { + boolean[] visited = new boolean[n]; + Deque stack = new ArrayDeque<>(); + stack.push(startNode); + visited[startNode] = true; + + while (!stack.isEmpty()) { + int u = stack.pop(); + for (int v : graph.getEdges(u)) { + if (!visited[v]) { + visited[v] = true; + stack.push(v); + } + } + for (int x = 0; x < n; x++) { + if (!visited[x]) { + for (int y : graph.getEdges(x)) { + if (y == u) { + visited[x] = true; + stack.push(x); + break; + } + } + } + } + } + + for (int i = 0; i < n; i++) { + if (outDegree[i] + inDegree[i] > 0 && !visited[i]) { + return false; + } + } + return true; + } +} diff --git a/src/test/java/com/thealgorithms/graph/HierholzerEulerianPathTest.java b/src/test/java/com/thealgorithms/graph/HierholzerEulerianPathTest.java new file mode 100644 index 000000000000..612d09561b7b --- /dev/null +++ b/src/test/java/com/thealgorithms/graph/HierholzerEulerianPathTest.java @@ -0,0 +1,169 @@ +package com.thealgorithms.graph; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link HierholzerEulerianPath}. + * + * This test suite validates Hierholzer's Algorithm implementation + * for finding Eulerian Paths and Circuits in directed graphs. + * + *

Coverage includes: + *

    + *
  • Basic Eulerian Circuit
  • + *
  • Eulerian Path
  • + *
  • Disconnected graphs
  • + *
  • Single-node graphs
  • + *
  • Graphs with no edges
  • + *
  • Graphs that do not have any Eulerian Path/Circuit
  • + *
+ *

+ */ +class HierholzerEulerianPathTest { + + @Test + void testSimpleEulerianCircuit() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(3); + graph.addEdge(0, 1); + graph.addEdge(1, 2); + graph.addEdge(2, 0); + + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + // Eulerian Circuit: [0, 1, 2, 0] + List expected = Arrays.asList(0, 1, 2, 0); + assertEquals(expected, result); + } + + @Test + void testEulerianPathDifferentStartEnd() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(4); + graph.addEdge(0, 1); + graph.addEdge(1, 2); + graph.addEdge(2, 3); + graph.addEdge(3, 1); + + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + // Eulerian Path: [0, 1, 2, 3, 1] + List expected = Arrays.asList(0, 1, 2, 3, 1); + assertEquals(expected, result); + } + + @Test + void testNoEulerianPathExists() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(3); + graph.addEdge(0, 1); + graph.addEdge(1, 2); + // Edge 2->0 missing, so it's not Eulerian Circuit + + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + assertEquals(result, Arrays.asList(0, 1, 2)); + } + + @Test + void testDisconnectedGraph() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(4); + graph.addEdge(0, 1); + graph.addEdge(2, 3); // disconnected component + + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + // Disconnected graph cannot have an Eulerian path + assertTrue(result.isEmpty()); + } + + @Test + void testGraphWithSelfLoop() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(3); + graph.addEdge(0, 0); // self loop + graph.addEdge(0, 1); + graph.addEdge(1, 2); + graph.addEdge(2, 0); + + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + // Eulerian Circuit with self-loop included: [0, 0, 1, 2, 0] + assertEquals(Arrays.asList(0, 0, 1, 2, 0), result); + } + + @Test + void testSingleNodeNoEdges() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(1); + + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + // Only one vertex and no edges + assertEquals(Collections.singletonList(0), result); + } + + @Test + void testSingleNodeWithLoop() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(1); + graph.addEdge(0, 0); + + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + // Eulerian circuit on a single node with a self-loop + assertEquals(Arrays.asList(0, 0), result); + } + + @Test + void testComplexEulerianCircuit() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(5); + graph.addEdge(0, 1); + graph.addEdge(1, 2); + graph.addEdge(2, 3); + graph.addEdge(3, 4); + graph.addEdge(4, 0); + graph.addEdge(1, 3); + graph.addEdge(3, 1); + + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + // Verify all edges are used + int totalEdges = 7; + assertEquals(totalEdges + 1, result.size(), "Path must contain all edges + 1 vertices"); + assertEquals(result.get(0), result.get(result.size() - 1), "Must form a circuit"); + } + + @Test + void testMultipleEdgesBetweenSameNodes() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(3); + graph.addEdge(0, 1); + graph.addEdge(0, 1); + graph.addEdge(1, 2); + graph.addEdge(2, 0); + + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + // Hava a Eulerian Path but not a Eulerian Circuit + assertEquals(result, Arrays.asList(0, 1, 2, 0, 1)); + } + + @Test + void testCoverageForEmptyGraph() { + HierholzerEulerianPath.Graph graph = new HierholzerEulerianPath.Graph(0); + HierholzerEulerianPath solver = new HierholzerEulerianPath(graph); + List result = solver.findEulerianPath(); + + // Empty graph has no vertices or path + assertTrue(result.isEmpty()); + } +}