Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(Elog<sub>E/V</sub>(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(V<sup>3</sup>)**
- [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))**
Expand Down
7 changes: 0 additions & 7 deletions src/main/java/com/williamfiset/algorithms/graphtheory/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Test against: https://open.kattis.com/problems/eulerianpath
* http://codeforces.com/contest/508/problem/D
* <p>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.
*
* <p>Run: bazel run //src/main/java/com/williamfiset/algorithms/graphtheory:EulerianPathDirectedEdgesAdjacencyList
* <p>Test against:
* <ul>
* <li>https://open.kattis.com/problems/eulerianpath
* <li>http://codeforces.com/contest/508/problem/D
* </ul>
*
* <p>Time Complexity: O(E)
* <p>Run with:
* bazel run //src/main/java/com/williamfiset/algorithms/graphtheory:EulerianPathDirectedEdgesAdjacencyList
*
* <p>Time Complexity: O(V + E)
*
* @see <a href="https://en.wikipedia.org/wiki/Eulerian_path">Eulerian Path (Wikipedia)</a>
* @author William Fiset, william.alexandre.fiset@gmail.com
*/
package com.williamfiset.algorithms.graphtheory;
Expand All @@ -20,47 +29,69 @@

public class EulerianPathDirectedEdgesAdjacencyList {

private final int n;
private int edgeCount;
private int[] in, out;
private LinkedList<Integer> path;
private List<List<Integer>> 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<Integer> path; // Stores the final Eulerian path
private List<List<Integer>> 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<List<Integer>> 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.
*
* <p>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.
*
* <p>Time: O(V + E)
* <p>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]++;
Expand All @@ -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.
*
* <p>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<List<Integer>> initializeEmptyGraph(int n) {
List<List<Integer>> 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<List<Integer>> g, int from, int to) {
g.get(from).add(to);
}
Expand Down Expand Up @@ -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() {
Expand All @@ -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));
}
}
Loading
Loading