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
100 changes: 100 additions & 0 deletions Python/data_structures/graphs/bellman_ford.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""
Bellman-Ford Algorithm
----------------------
Description:
Computes shortest paths from a single source vertex to all other vertices in a weighted graph.
Supports negative weight edges and detects negative weight cycles.

Usage:
Call bellman_ford(vertices, edges, source) with:
- vertices: list of node labels
- edges: list of tuples (u, v, weight)
- source: starting node label

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


"""

def bellman_ford(vertices, edges, source):
# Validate that the source exists in the graph
if source not in vertices:
raise ValueError(f"Source vertex '{source}' not found in graph.")

# Validate that all edge endpoints exist in the graph
for u, v, _ in edges:
if u not in vertices or v not in vertices:
raise ValueError(f"Edge contains undefined vertex: ({u}, {v})")

# Step 1: Initialize distances from source to all vertices as infinite
distance = {v: float('inf') for v in vertices}
distance[source] = 0

# Step 2: Relax all edges |V| - 1 times
for _ in range(len(vertices) - 1):
for u, v, w in edges:
# If the current path offers a shorter route, update the distance
if distance[u] != float('inf') and distance[u] + w < distance[v]:
distance[v] = distance[u] + w

# Step 3: Check for negative weight cycles
for u, v, w in edges:
if distance[u] != float('inf') and distance[u] + w < distance[v]:
raise ValueError("Graph contains a negative weight cycle.")

return distance


# ------------------ Test Cases ------------------

def run_tests():
print("Running Bellman-Ford test cases...\n")

# Test Case 1: Basic graph with positive weights
vertices1 = ['A', 'B', 'C', 'D']
edges1 = [
('A', 'B', 1),
('B', 'C', 3),
('A', 'C', 10),
('C', 'D', 2),
('B', 'D', 2)
]
print("Test Case 1:", bellman_ford(vertices1, edges1, 'A'))

# Test Case 2: Graph with negative weights but no cycle
vertices2 = ['X', 'Y', 'Z']
edges2 = [
('X', 'Y', 4),
('Y', 'Z', -2),
('X', 'Z', 5)
]
print("Test Case 2:", bellman_ford(vertices2, edges2, 'X'))

# Test Case 3: Graph with a negative weight cycle
vertices3 = ['P', 'Q', 'R']
edges3 = [
('P', 'Q', 1),
('Q', 'R', -1),
('R', 'P', -1)
]
try:
print("Test Case 3:", bellman_ford(vertices3, edges3, 'P'))
except ValueError as e:
print("Test Case 3: Exception caught -", e)

# Test Case 4: Invalid source vertex
try:
print("Test Case 4:", bellman_ford(['A', 'B'], [('A', 'B', 2)], 'Z'))
except ValueError as e:
print("Test Case 4: Exception caught -", e)

# Test Case 5: Edge with undefined vertex
try:
print("Test Case 5:", bellman_ford(['A', 'B'], [('A', 'C', 2)], 'A'))
except ValueError as e:
print("Test Case 5: Exception caught -", e)


if __name__ == "__main__":
run_tests()
97 changes: 97 additions & 0 deletions Python/data_structures/graphs/floyd_warshall.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""
Floyd-Warshall Algorithm
------------------------
Description:
Computes shortest paths between all pairs of vertices in a weighted graph using dynamic programming.
Supports negative edge weights but does not detect negative weight cycles.

Algorithm:
- Initialize a distance matrix with infinity for all pairs except self-loops (0).
- Populate the matrix with direct edge weights.
- For each intermediate vertex k, update dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]).

Time Complexity: O(V^3)
Space Complexity: O(V^2)

"""

def floyd_warshall(vertices, edges):
# Validate that vertex list is not empty
if not vertices:
raise ValueError("Vertex list is empty.")

# Validate that all edge endpoints exist in the graph
for u, v, _ in edges:
if u not in vertices or v not in vertices:
raise ValueError(f"Edge contains undefined vertex: ({u}, {v})")

# Step 1: Initialize distance matrix with infinity
dist = {i: {j: float('inf') for j in vertices} for i in vertices}
for v in vertices:
dist[v][v] = 0 # Distance to self is zero

# Step 2: Populate initial edge weights
for u, v, w in edges:
dist[u][v] = w

# Step 3: Floyd-Warshall core logic
for k in vertices:
for i in vertices:
for j in vertices:
# If going through k offers a shorter path, update it
if dist[i][k] + dist[k][j] < dist[i][j]:
dist[i][j] = dist[i][k] + dist[k][j]

return dist


# ------------------ Test Cases ------------------

def run_tests():
print("Running Floyd-Warshall test cases...\n")

# Test Case 1: Basic graph
vertices1 = ['A', 'B', 'C']
edges1 = [
('A', 'B', 4),
('B', 'C', 1),
('A', 'C', 10)
]
print("Test Case 1:")
print(floyd_warshall(vertices1, edges1), "\n")

# Test Case 2: Graph with negative weights
vertices2 = ['X', 'Y', 'Z']
edges2 = [
('X', 'Y', 3),
('Y', 'Z', -2),
('Z', 'X', 5)
]
print("Test Case 2:")
print(floyd_warshall(vertices2, edges2), "\n")

# Test Case 3: Disconnected graph
vertices3 = ['P', 'Q', 'R']
edges3 = [
('P', 'Q', 7)
]
print("Test Case 3:")
print(floyd_warshall(vertices3, edges3), "\n")

# Test Case 4: Invalid edge vertex
try:
print("Test Case 4:")
floyd_warshall(['A', 'B'], [('A', 'C', 2)])
except ValueError as e:
print("Exception caught -", e, "\n")

# Test Case 5: Empty vertex list
try:
print("Test Case 5:")
floyd_warshall([], [('A', 'B', 1)])
except ValueError as e:
print("Exception caught -", e, "\n")


if __name__ == "__main__":
run_tests()
94 changes: 94 additions & 0 deletions Python/data_structures/graphs/topological_sort.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""
Topological Sort (DFS-based)
----------------------------
Description:
Performs topological sorting on a Directed Acyclic Graph (DAG) using Depth-First Search.
Returns a linear ordering of vertices such that for every directed edge u → v, u comes before v.

Algorithm:
- Build an adjacency list from the edge list.
- Use DFS to explore each node.
- After visiting all descendants, push the node onto a stack.
- Reverse the stack to get the topological order.

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

"""

def topological_sort(vertices, edges):
# Validate that vertex list is not empty
if not vertices:
raise ValueError("Vertex list is empty.")

# Validate that all edge endpoints exist in the graph
for u, v in edges:
if u not in vertices or v not in vertices:
raise ValueError(f"Edge contains undefined vertex: ({u}, {v})")

# Step 1: Build adjacency list
adj = {v: [] for v in vertices}
for u, v in edges:
adj[u].append(v)

visited = set()
stack = []

# Step 2: Depth-First Search helper
def dfs(node):
visited.add(node)
for neighbor in adj[node]:
if neighbor not in visited:
dfs(neighbor)
stack.append(node) # Post-order push

# Step 3: Perform DFS from all unvisited nodes
for v in vertices:
if v not in visited:
dfs(v)

return stack[::-1] # Reverse to get topological order


# ------------------ Test Cases ------------------

def run_tests():
print("Running Topological Sort test cases...\n")

# Test Case 1: Simple DAG
vertices1 = ['A', 'B', 'C', 'D']
edges1 = [('A', 'B'), ('B', 'C'), ('A', 'C'), ('C', 'D')]
print("Test Case 1:")
print(topological_sort(vertices1, edges1), "\n")

# Test Case 2: DAG with branching
vertices2 = ['X', 'Y', 'Z', 'W']
edges2 = [('X', 'Y'), ('X', 'Z'), ('Y', 'W'), ('Z', 'W')]
print("Test Case 2:")
print(topological_sort(vertices2, edges2), "\n")

# Test Case 3: Disconnected DAG
vertices3 = ['P', 'Q', 'R', 'S']
edges3 = [('P', 'Q'), ('R', 'S')]
print("Test Case 3:")
print(topological_sort(vertices3, edges3), "\n")

# Test Case 4: Empty graph
vertices4 = []
edges4 = []
try:
print("Test Case 4:")
topological_sort(vertices4, edges4)
except ValueError as e:
print("Exception caught -", e, "\n")

# Test Case 5: Invalid edge vertex
try:
print("Test Case 5:")
topological_sort(['A', 'B'], [('A', 'C')])
except ValueError as e:
print("Exception caught -", e, "\n")


if __name__ == "__main__":
run_tests()