Skip to content

Commit

Permalink
Merge pull request #24 from MIT-Emerging-Talent/eulerian_path
Browse files Browse the repository at this point in the history
Eulerian path
  • Loading branch information
KhalidOmer authored Jan 22, 2024
2 parents 0074f8d + 55549bf commit 6b5f804
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 0 deletions.
51 changes: 51 additions & 0 deletions src/graphs/python/eulerian_path/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# The Eulerian Path problem

Given a graph, is it possible to traverse every edge exactly once, starting and ending at any vertex?

## Contents

- [Introduction]
- [Implementation]
- [Behavior]
- [Test Cases]
- [Running Tests]

## Introduction

For a graph to have an Eulerian path, it must satisfy the following:

a- Connectedness: The graph must be connected. In the context of Eulerian circuits, it is also required that every vertex has even degree (an even number of edges connected to it).

b- Eulerian Path: If there are exactly two vertices (nodes) with an odd degree in the graph, an Eulerian path is possible, and those two vertices (nodes) will be the start and end points of the path. If there are no vertices (nodes) with an odd degree, an Eulerian circuit is possible.

## Implementation

The first step in the implementation is pick the algorithm, in the case here, Fleury's algorithm, generally this problem can sovled as follows, be a ware that the implementation follows the solution:

1 - First we check the connectedness of the graph, a graph is considered connnected if there is a path between each pair of nodes, if the graph is not connected then there will not an Eulerian Path.
2- Degrees of the nodes of the graph, it is a number that represents the number of edges that are connected to a given node for a graph is there are two exactly twp vertices (nodes) with odd degrees, then and Euler path is possible.
3 - Finally if our graph passes the tests above we can construct an Eulerian graph.
4- Un important concept emerges here, which the concept of the "bridge", and edge is considered a bridge if by removing it, the graph becomes disconneted, i.e. it is same idea as the idea of a bridge in a city, so in our implementation, we want to be sure that removeing an edge will not affect the graph.
5 - To implement Fleury's algorithm here, we utalize the DFS algorithm, we use this function to check the connectivity of the graph as well as checking for bridges.
6 - The graphs are represented by the adjacency list, here a dictionary is utlized where, the keys represent the nodes and values are list of the nodes that are connected to that node

## Behavior

1- Two helper functions are written to check connectivity of the graph, a starting point is chosen, then iterate through the graph until no edge is left, in each iteration select an edge that does not disconnect the graph, remove it from the graph.
2 - The selected edges form the Eulerian path.

## Test Cases
### Test 1: Eulerian Path Exists
- This test checks scenarios where an Eulerian path should exist in the graph.
- **Test Cases:**
1. A graph with vertices `[0, 1, 2, 3]` where each vertex has degree 3.
2. A graph with vertices `[0, 1, 2, 3, 4]` where vertices 1 and 2 have degree 2, and the rest have degree 3.

### Test 2: No Eulerian Path Exists
- This test checks scenarios where no Eulerian path exists in the graph.
- **Test Cases:**
1. A complete graph with vertices `[0, 1, 2]` where each vertex has degree 2.
2. A graph with vertices `[0, 1, 2, 3, 4]` where vertices 3 and 4 are connected to form an isolated component.

## Running tests
- to run the tests execute python test_eulerian_path.py
43 changes: 43 additions & 0 deletions src/graphs/python/eulerian_path/src/eulerian_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Import the is_connected and is_bridge functions
from is_connected import is_bridge, is_connected

# Implement the Fleury algorithm using a function named eulerian_path
# The function takes a graph as a parameter
# the function returns the eulerian path as list
def eulerian_path(graph):
# Create a copy of the graph
graph_copy = {u: list(neighbors) for u, neighbors in graph.items()}

# Calculate Odd-Degree Nodes for an Eulerian Path
odd_degree_nodes = [u for u in graph if len(graph[u]) % 2 != 0]

# Check Eulerian Path Second Condition
if len(odd_degree_nodes) != 0 and len(odd_degree_nodes) != 2:
return "No Eulerian path exists"

# Choose a starting node
# The use of next(iter(graph)) here to handle the case of big graphs
start_node = odd_degree_nodes[0] if odd_degree_nodes else next(iter(graph))

path = [start_node]
#current_node = start_node

while graph_copy:
current_node = path[-1]
for neighbor in graph_copy[path[-1]]:
if not is_bridge(path[-1], neighbor, graph_copy):
# Remove the edge
graph_copy[path[-1]].remove(neighbor)
graph_copy[neighbor].remove(path[-1])
# Move to the next vertex
path.append(neighbor)
break

if path[-1] == current_node:
# This means we have not moved to a new vertex
break


return path


55 changes: 55 additions & 0 deletions src/graphs/python/eulerian_path/src/is_connected.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
def is_connected(graph, u, v):
'''
Check connectivity using Depth-First Search (DFS)
Args:
graph: a graph represented by the adjacency list
u, v: nodes on the graph
'''
# Pick a starting node; check for its degree first.
# If greater than one, it's a good start.
# The logic is that a node with a higher degree is likely to cover more of the graph quickly in DFS.
if len(graph[u]) > 1:
start_node = u
else:
start_node = v

# Store the visited nodes in a set; a set discards duplicates efficiently.
visited = set()

# The DFS function takes a par. node and explores its neighbours recursively.
def dfs(node):
# Add the current node to visited nodes.
visited.add(node)
# Explore the neighbors of the node.
for neighbor in graph[node]:
if neighbor not in visited:
dfs(neighbor)

# Initiate the DFS search inside the is_connected function.
dfs(start_node)
return len(visited) == len(graph)

def is_bridge(u, v, graph):
'''
Check if an edge is a bridge.
Args:
u, v: nodes
graph: graph represented using the adjacency list
Returns:
True if removing an edge makes the graph disconnected, meaning that it is a bridge.
'''
# Create a copy of the graph to avoid modifying the original graph.
graph_copy = {node: neighbors[:] for node, neighbors in graph.items()}

# Temporarily remove the edge (u, v) and check connectivity.
graph_copy[u].remove(v)
graph_copy[v].remove(u)

# Use DFS for connectivity check.
connected = is_connected(graph_copy, u, v)

# Restore the removed edge.
graph_copy[u].append(v)
graph_copy[v].append(u)

return not connected
26 changes: 26 additions & 0 deletions src/graphs/python/eulerian_path/src/test_eulerian_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# test_eulerian_path.py

import unittest
from eulerian_path import eulerian_path

class TestEulerianPath(unittest.TestCase):
def test_eulerian_path_exists(self):
# Test cases where Eulerian paths exist
graph1 = {1: [2, 3], 2: [1, 3, 4], 3: [1, 2, 4], 4: [2, 3]}
self.assertTrue(eulerian_path(graph1))

graph2 = {0: [1, 2, 3, 4], 1: [0, 2, 3, 4],
2: [0, 1, 3, 4], 3: [0, 1, 2, 4],
4: [0, 1, 2, 3]}
self.assertTrue(eulerian_path(graph2))

def test_no_eulerian_path(self):
# Test cases where no Eulerian path exists
graph3 = {0: [1, 2], 1: [0, 2], 2: [0, 1, 3], 3: [2, 4], 4: []}
self.assertEqual(eulerian_path(graph3), "No Eulerian path exists")

graph4 = {0: [1, 2, 3], 1: [0, 2, 3], 2: [0, 1, 3], 3: [0, 1, 2], 4: []}
self.assertEqual(eulerian_path(graph4), "No Eulerian path exists")

if __name__ == "__main__":
unittest.main()

0 comments on commit 6b5f804

Please sign in to comment.