generated from MIT-Emerging-Talent/Algorithms-And-Data-Structures
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #24 from MIT-Emerging-Talent/eulerian_path
Eulerian path
- Loading branch information
Showing
4 changed files
with
175 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |