Skip to content

Commit 8402d30

Browse files
[pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
1 parent 6ddbb09 commit 8402d30

11 files changed

+424
-385
lines changed

graphs/chinese_postman.py

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,92 +17,93 @@ class ChinesePostman:
1717
"""
1818
Solve Chinese Postman Problem for weighted undirected graphs.
1919
"""
20-
20+
2121
def __init__(self, n: int):
2222
self.n = n
2323
self.adj: List[List[Tuple[int, int]]] = [[] for _ in range(n)]
2424
self.total_weight = 0
25-
25+
2626
def add_edge(self, u: int, v: int, w: int) -> None:
2727
"""Add undirected edge."""
2828
self.adj[u].append((v, w))
2929
self.adj[v].append((u, w))
3030
self.total_weight += w
31-
31+
3232
def _floyd_warshall(self) -> List[List[float]]:
3333
"""All-pairs shortest paths."""
3434
n = self.n
35-
dist = [[float('inf')] * n for _ in range(n)]
36-
35+
dist = [[float("inf")] * n for _ in range(n)]
36+
3737
for i in range(n):
3838
dist[i][i] = 0
39-
39+
4040
for u in range(n):
4141
for v, w in self.adj[u]:
4242
dist[u][v] = min(dist[u][v], w)
43-
43+
4444
for k in range(n):
4545
for i in range(n):
4646
for j in range(n):
4747
if dist[i][k] + dist[k][j] < dist[i][j]:
4848
dist[i][j] = dist[i][k] + dist[k][j]
49-
49+
5050
return dist
51-
51+
5252
def _find_odd_degree_vertices(self) -> List[int]:
5353
"""Find vertices with odd degree."""
5454
odd = []
5555
for u in range(self.n):
5656
if len(self.adj[u]) % 2 == 1:
5757
odd.append(u)
5858
return odd
59-
60-
def _min_weight_perfect_matching(self, odd_vertices: List[int],
61-
dist: List[List[float]]) -> float:
59+
60+
def _min_weight_perfect_matching(
61+
self, odd_vertices: List[int], dist: List[List[float]]
62+
) -> float:
6263
"""
6364
Find minimum weight perfect matching on odd degree vertices.
6465
Uses brute force for small k (k <= 20), which is practical.
6566
"""
6667
k = len(odd_vertices)
6768
if k == 0:
6869
return 0
69-
70+
7071
# Dynamic programming: dp[mask] = min cost to match vertices in mask
7172
dp: Dict[int, float] = {0: 0}
72-
73+
7374
for mask in range(1 << k):
74-
if bin(mask).count('1') % 2 == 1:
75+
if bin(mask).count("1") % 2 == 1:
7576
continue # Odd number of bits, can't be perfectly matched
76-
77+
7778
if mask not in dp:
7879
continue
79-
80+
8081
# Find first unset bit
8182
i = 0
8283
while i < k and (mask & (1 << i)):
8384
i += 1
84-
85+
8586
if i >= k:
8687
continue
87-
88+
8889
# Try matching i with every other unmatched vertex j
8990
for j in range(i + 1, k):
9091
if not (mask & (1 << j)):
9192
new_mask = mask | (1 << i) | (1 << j)
9293
cost = dp[mask] + dist[odd_vertices[i]][odd_vertices[j]]
9394
if new_mask not in dp or cost < dp[new_mask]:
9495
dp[new_mask] = cost
95-
96+
9697
full_mask = (1 << k) - 1
9798
return dp.get(full_mask, 0)
98-
99+
99100
def solve(self) -> Tuple[float, List[int]]:
100101
"""
101102
Solve Chinese Postman Problem.
102-
103+
103104
Returns:
104105
Tuple of (minimum_cost, eulerian_circuit)
105-
106+
106107
Example:
107108
>>> cpp = ChinesePostman(4)
108109
>>> cpp.add_edge(0, 1, 1)
@@ -115,40 +116,41 @@ def solve(self) -> Tuple[float, List[int]]:
115116
"""
116117
# Find odd degree vertices
117118
odd_vertices = self._find_odd_degree_vertices()
118-
119+
119120
# Graph is already Eulerian
120121
if len(odd_vertices) == 0:
121122
circuit = self._find_eulerian_circuit()
122123
return float(self.total_weight), circuit
123-
124+
124125
# Compute all-pairs shortest paths
125126
dist = self._floyd_warshall()
126-
127+
127128
# Find minimum weight matching
128129
matching_cost = self._min_weight_perfect_matching(odd_vertices, dist)
129-
130+
130131
# Duplicate edges from matching to make graph Eulerian
131132
self._add_matching_edges(odd_vertices, dist)
132-
133+
133134
# Find Eulerian circuit
134135
circuit = self._find_eulerian_circuit()
135-
136+
136137
return float(self.total_weight + matching_cost), circuit
137-
138-
def _add_matching_edges(self, odd_vertices: List[int],
139-
dist: List[List[float]]) -> None:
138+
139+
def _add_matching_edges(
140+
self, odd_vertices: List[int], dist: List[List[float]]
141+
) -> None:
140142
"""Duplicate edges based on minimum matching (simplified)."""
141143
# In practice, reconstruct path and add edges
142144
# For this implementation, we assume edges can be duplicated
143145
pass
144-
146+
145147
def _find_eulerian_circuit(self) -> List[int]:
146148
"""Find Eulerian circuit using Hierholzer's algorithm."""
147149
n = self.n
148150
adj_copy = [list(neighbors) for neighbors in self.adj]
149151
circuit = []
150152
stack = [0]
151-
153+
152154
while stack:
153155
u = stack[-1]
154156
if adj_copy[u]:
@@ -161,18 +163,20 @@ def _find_eulerian_circuit(self) -> List[int]:
161163
stack.append(v)
162164
else:
163165
circuit.append(stack.pop())
164-
166+
165167
return circuit[::-1]
166168

167169

168-
def chinese_postman(n: int, edges: List[Tuple[int, int, int]]) -> Tuple[float, List[int]]:
170+
def chinese_postman(
171+
n: int, edges: List[Tuple[int, int, int]]
172+
) -> Tuple[float, List[int]]:
169173
"""
170174
Convenience function for Chinese Postman.
171-
175+
172176
Args:
173177
n: Number of vertices
174178
edges: List of (u, v, weight) undirected edges
175-
179+
176180
Returns:
177181
(minimum_cost, eulerian_circuit)
178182
"""
@@ -184,4 +188,5 @@ def chinese_postman(n: int, edges: List[Tuple[int, int, int]]) -> Tuple[float, L
184188

185189
if __name__ == "__main__":
186190
import doctest
187-
doctest.testmod()
191+
192+
doctest.testmod()

graphs/floyd_warshall.py

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,21 @@
1111
from typing import List, Tuple, Optional
1212

1313

14-
def floyd_warshall(graph: List[List[float]]) -> Tuple[List[List[float]], List[List[Optional[int]]]]:
14+
def floyd_warshall(
15+
graph: List[List[float]],
16+
) -> Tuple[List[List[float]], List[List[Optional[int]]]]:
1517
"""
1618
Compute all-pairs shortest paths using Floyd-Warshall algorithm.
17-
19+
1820
Args:
1921
graph: Adjacency matrix where graph[i][j] is weight from i to j.
2022
Use float('inf') for no edge. graph[i][i] should be 0.
21-
23+
2224
Returns:
2325
Tuple of (distance_matrix, next_matrix)
2426
- distance_matrix[i][j] = shortest distance from i to j
2527
- next_matrix[i][j] = next node to visit from i to reach j optimally
26-
28+
2729
Example:
2830
>>> graph = [[0, 3, float('inf'), 7],
2931
... [8, 0, 2, float('inf')],
@@ -34,90 +36,96 @@ def floyd_warshall(graph: List[List[float]]) -> Tuple[List[List[float]], List[Li
3436
6
3537
"""
3638
n = len(graph)
37-
39+
3840
# Initialize distance and path matrices
3941
dist = [row[:] for row in graph] # Deep copy
40-
next_node = [[j if graph[i][j] != float('inf') and i != j else None
41-
for j in range(n)] for i in range(n)]
42-
42+
next_node = [
43+
[j if graph[i][j] != float("inf") and i != j else None for j in range(n)]
44+
for i in range(n)
45+
]
46+
4347
# Main algorithm: try each vertex as intermediate
4448
for k in range(n):
4549
for i in range(n):
4650
for j in range(n):
4751
if dist[i][k] + dist[k][j] < dist[i][j]:
4852
dist[i][j] = dist[i][k] + dist[k][j]
4953
next_node[i][j] = next_node[i][k]
50-
54+
5155
# Check for negative cycles
5256
for i in range(n):
5357
if dist[i][i] < 0:
5458
raise ValueError("Graph contains negative weight cycle")
55-
59+
5660
return dist, next_node
5761

5862

59-
def reconstruct_path(next_node: List[List[Optional[int]]],
60-
start: int, end: int) -> Optional[List[int]]:
63+
def reconstruct_path(
64+
next_node: List[List[Optional[int]]], start: int, end: int
65+
) -> Optional[List[int]]:
6166
"""
6267
Reconstruct shortest path from start to end using next_node matrix.
63-
68+
6469
Time Complexity: O(V)
6570
"""
6671
if next_node[start][end] is None:
6772
return None
68-
73+
6974
path = [start]
7075
current = start
71-
76+
7277
while current != end:
7378
current = next_node[current][end] # type: ignore
7479
path.append(current)
75-
80+
7681
return path
7782

7883

7984
def floyd_warshall_optimized(graph: List[List[float]]) -> List[List[float]]:
8085
"""
8186
Space-optimized version using only distance matrix.
8287
Use when path reconstruction is not needed.
83-
88+
8489
Time Complexity: O(V³)
8590
Space Complexity: O(V²) but less overhead
8691
"""
8792
n = len(graph)
8893
dist = [row[:] for row in graph]
89-
94+
9095
for k in range(n):
9196
for i in range(n):
92-
if dist[i][k] == float('inf'):
97+
if dist[i][k] == float("inf"):
9398
continue
9499
for j in range(n):
95-
if dist[k][j] == float('inf'):
100+
if dist[k][j] == float("inf"):
96101
continue
97102
new_dist = dist[i][k] + dist[k][j]
98103
if new_dist < dist[i][j]:
99104
dist[i][j] = new_dist
100-
105+
101106
return dist
102107

103108

104109
if __name__ == "__main__":
105110
import doctest
111+
106112
doctest.testmod()
107-
113+
108114
# Performance benchmark
109115
import random
110116
import time
111-
117+
112118
def benchmark():
113119
n = 200
114120
# Generate random dense graph
115-
graph = [[0 if i == j else random.randint(1, 100)
116-
for j in range(n)] for i in range(n)]
117-
121+
graph = [
122+
[0 if i == j else random.randint(1, 100) for j in range(n)]
123+
for i in range(n)
124+
]
125+
118126
start = time.perf_counter()
119127
floyd_warshall(graph)
120128
elapsed = time.perf_counter() - start
121129
print(f"Floyd-Warshall on {n}x{n} graph: {elapsed:.3f}s")
122-
123-
benchmark()
130+
131+
benchmark()

0 commit comments

Comments
 (0)