Skip to content

Commit 6161141

Browse files
TarunVishwakarma1pre-commit-ci[bot]cclauss
authored
Added edmonds_blossom_algorithm.py. For maximum matching in the graph. #12043 (#12056)
* Added edmonds_blossom_algorithm.py. For maximum matching in the graph. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Added test for edmonds_blossom_algorithm.py in graph/tests/ Resolved the blossom data naming issue. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * resolved pre commit checks for test_edmonds_blossom_algorithm.py and edmonds_blossom_algorithm.py * Changes in the main file and test file as test were failing due to stuck in an infinite loop. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Resolved Pre commit errors * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Resolved per commit checks and unresolved conversations * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * updating DIRECTORY.md --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Christian Clauss <cclauss@me.com> Co-authored-by: cclauss <cclauss@users.noreply.github.com>
1 parent 7cf493e commit 6161141

3 files changed

Lines changed: 341 additions & 0 deletions

File tree

‎DIRECTORY.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,7 @@
631631
* [Dijkstra Binary Grid](graphs/dijkstra_binary_grid.py)
632632
* [Dinic](graphs/dinic.py)
633633
* [Directed And Undirected Weighted Graph](graphs/directed_and_undirected_weighted_graph.py)
634+
* [Edmonds Blossom Algorithm](graphs/edmonds_blossom_algorithm.py)
634635
* [Edmonds Karp Multiple Source And Sink](graphs/edmonds_karp_multiple_source_and_sink.py)
635636
* [Eulerian Path And Circuit For Undirected Graph](graphs/eulerian_path_and_circuit_for_undirected_graph.py)
636637
* [Even Tree](graphs/even_tree.py)
@@ -668,6 +669,7 @@
668669
* [Strongly Connected Components](graphs/strongly_connected_components.py)
669670
* [Tarjans Scc](graphs/tarjans_scc.py)
670671
* Tests
672+
* [Test Edmonds Blossom Algorithm](graphs/tests/test_edmonds_blossom_algorithm.py)
671673
* [Test Graphs Floyd Warshall](graphs/tests/test_graphs_floyd_warshall.py)
672674
* [Test Johnson](graphs/tests/test_johnson.py)
673675
* [Test Min Spanning Tree Kruskal](graphs/tests/test_min_spanning_tree_kruskal.py)
Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,267 @@
1+
from collections import deque
2+
3+
4+
class BlossomAuxData:
5+
"""Class to hold auxiliary data during the blossom algorithm's execution."""
6+
7+
def __init__(
8+
self,
9+
queue: deque,
10+
parent: list[int],
11+
base: list[int],
12+
in_blossom: list[bool],
13+
match: list[int],
14+
in_queue: list[bool],
15+
) -> None:
16+
"""
17+
Initializes the BlossomAuxData instance.
18+
19+
Args:
20+
queue: A deque for BFS processing.
21+
parent: List of parent vertices in the augmenting path.
22+
base: List of base vertices for each vertex.
23+
in_blossom: Boolean list indicating if a vertex is in a blossom.
24+
match: List of matched vertices.
25+
in_queue: Boolean list indicating if a vertex is in the queue.
26+
"""
27+
self.queue = queue
28+
self.parent = parent
29+
self.base = base
30+
self.in_blossom = in_blossom
31+
self.match = match
32+
self.in_queue = in_queue
33+
34+
35+
class BlossomData:
36+
"""Class to encapsulate data related to a blossom in the graph."""
37+
38+
def __init__(
39+
self,
40+
aux_data: BlossomAuxData,
41+
vertex_u: int,
42+
vertex_v: int,
43+
lowest_common_ancestor: int,
44+
) -> None:
45+
"""
46+
Initializes the BlossomData instance.
47+
48+
Args:
49+
aux_data: The auxiliary data related to the blossom.
50+
vertex_u: One vertex in the blossom.
51+
vertex_v: The other vertex in the blossom.
52+
lowest_common_ancestor: The lowest common ancestor of vertex_u and vertex_v.
53+
"""
54+
self.aux_data = aux_data
55+
self.vertex_u = vertex_u
56+
self.vertex_v = vertex_v
57+
self.lowest_common_ancestor = lowest_common_ancestor
58+
59+
60+
class EdmondsBlossomAlgorithm:
61+
UNMATCHED = -1 # Constant to represent unmatched vertices
62+
63+
@staticmethod
64+
def maximum_matching(edges: list[list[int]], vertex_count: int) -> list[list[int]]:
65+
"""
66+
Finds the maximum matching in a graph using the Edmonds Blossom Algorithm.
67+
68+
Args:
69+
edges: A list of edges represented as pairs of vertices.
70+
vertex_count: The total number of vertices in the graph.
71+
72+
Returns:
73+
A list of matched pairs in the form of a list of lists.
74+
"""
75+
# Create an adjacency list for the graph
76+
graph: list[list[int]] = [[] for _ in range(vertex_count)]
77+
78+
# Populate the graph with the edges
79+
for edge in edges:
80+
u, v = edge
81+
graph[u].append(v)
82+
graph[v].append(u)
83+
84+
# All vertices are initially unmatched
85+
match: list[int] = [EdmondsBlossomAlgorithm.UNMATCHED] * vertex_count
86+
parent: list[int] = [EdmondsBlossomAlgorithm.UNMATCHED] * vertex_count
87+
# Each vertex is its own base initially
88+
base: list[int] = list(range(vertex_count))
89+
in_blossom: list[bool] = [False] * vertex_count
90+
# Tracks vertices in the BFS queue
91+
in_queue: list[bool] = [False] * vertex_count
92+
93+
# Main logic for finding maximum matching
94+
for u in range(vertex_count):
95+
# Only consider unmatched vertices
96+
if match[u] == EdmondsBlossomAlgorithm.UNMATCHED:
97+
# BFS initialization
98+
parent = [EdmondsBlossomAlgorithm.UNMATCHED] * vertex_count
99+
base = list(range(vertex_count))
100+
in_blossom = [False] * vertex_count
101+
in_queue = [False] * vertex_count
102+
103+
queue = deque([u]) # Start BFS from the unmatched vertex
104+
in_queue[u] = True
105+
106+
augmenting_path_found = False
107+
108+
# BFS to find augmenting paths
109+
while queue and not augmenting_path_found:
110+
current = queue.popleft() # Get the current vertex
111+
for y in graph[current]: # Explore adjacent vertices
112+
# Skip if we're looking at the current match
113+
if match[current] == y:
114+
continue
115+
116+
if base[current] == base[y]: # Avoid self-loops
117+
continue
118+
119+
if parent[y] == EdmondsBlossomAlgorithm.UNMATCHED:
120+
# Case 1: y is unmatched;
121+
# we've found an augmenting path
122+
if match[y] == EdmondsBlossomAlgorithm.UNMATCHED:
123+
parent[y] = current # Update the parent
124+
augmenting_path_found = True
125+
# Augment along this path
126+
EdmondsBlossomAlgorithm.update_matching(
127+
match, parent, y
128+
)
129+
break
130+
131+
# Case 2: y is matched;
132+
# add y's match to the queue
133+
z = match[y]
134+
parent[y] = current
135+
parent[z] = y
136+
if not in_queue[z]: # If z is not already in the queue
137+
queue.append(z)
138+
in_queue[z] = True
139+
else:
140+
# Case 3: Both current and y have a parent;
141+
# check for a cycle/blossom
142+
base_u = EdmondsBlossomAlgorithm.find_base(
143+
base, parent, current, y
144+
)
145+
if base_u != EdmondsBlossomAlgorithm.UNMATCHED:
146+
EdmondsBlossomAlgorithm.contract_blossom(
147+
BlossomData(
148+
BlossomAuxData(
149+
queue,
150+
parent,
151+
base,
152+
in_blossom,
153+
match,
154+
in_queue,
155+
),
156+
current,
157+
y,
158+
base_u,
159+
)
160+
)
161+
162+
# Create result list of matched pairs
163+
matching_result: list[list[int]] = []
164+
for v in range(vertex_count):
165+
if (
166+
match[v] != EdmondsBlossomAlgorithm.UNMATCHED and v < match[v]
167+
): # Ensure pairs are unique
168+
matching_result.append([v, match[v]])
169+
170+
return matching_result
171+
172+
@staticmethod
173+
def update_matching(
174+
match: list[int], parent: list[int], matched_vertex: int
175+
) -> None:
176+
"""
177+
Updates the matching based on the augmenting path found.
178+
179+
Args:
180+
match: The current match list.
181+
parent: The parent list from BFS traversal.
182+
matched_vertex: The vertex where the augmenting path ends.
183+
"""
184+
while matched_vertex != EdmondsBlossomAlgorithm.UNMATCHED:
185+
v = parent[matched_vertex] # Get the parent vertex
186+
next_match = match[v] # Store the next match
187+
match[v] = matched_vertex # Update match for v
188+
match[matched_vertex] = v # Update match for matched_vertex
189+
matched_vertex = next_match # Move to the next vertex
190+
191+
@staticmethod
192+
def find_base(
193+
base: list[int], parent: list[int], vertex_u: int, vertex_v: int
194+
) -> int:
195+
"""
196+
Finds the base of the blossom.
197+
198+
Args:
199+
base: The base array for each vertex.
200+
parent: The parent array from BFS.
201+
vertex_u: One endpoint of the blossom.
202+
vertex_v: The other endpoint of the blossom.
203+
204+
Returns:
205+
The lowest common ancestor of vertex_u and vertex_v in the blossom.
206+
"""
207+
visited: list[bool] = [False] * len(base)
208+
209+
# Mark ancestors of vertex_u
210+
current_vertex_u = vertex_u
211+
while True:
212+
current_vertex_u = base[current_vertex_u]
213+
# Mark this base as visited
214+
visited[current_vertex_u] = True
215+
if parent[current_vertex_u] == EdmondsBlossomAlgorithm.UNMATCHED:
216+
break
217+
current_vertex_u = parent[current_vertex_u]
218+
219+
# Find the common ancestor of vertex_v
220+
current_vertex_v = vertex_v
221+
while True:
222+
current_vertex_v = base[current_vertex_v]
223+
# Check if we've already visited this base
224+
if visited[current_vertex_v]:
225+
return current_vertex_v
226+
current_vertex_v = parent[current_vertex_v]
227+
228+
@staticmethod
229+
def contract_blossom(blossom_data: BlossomData) -> None:
230+
"""
231+
Contracts a blossom found during the matching process.
232+
233+
Args:
234+
blossom_data: The data related to the blossom to be contracted.
235+
"""
236+
# Mark vertices in the blossom
237+
for x in range(
238+
blossom_data.vertex_u,
239+
blossom_data.aux_data.base[blossom_data.vertex_u]
240+
!= blossom_data.lowest_common_ancestor,
241+
):
242+
base_x = blossom_data.aux_data.base[x]
243+
match_base_x = blossom_data.aux_data.base[blossom_data.aux_data.match[x]]
244+
# Mark the base as in a blossom
245+
blossom_data.aux_data.in_blossom[base_x] = True
246+
blossom_data.aux_data.in_blossom[match_base_x] = True
247+
248+
for x in range(
249+
blossom_data.vertex_v,
250+
blossom_data.aux_data.base[blossom_data.vertex_v]
251+
!= blossom_data.lowest_common_ancestor,
252+
):
253+
base_x = blossom_data.aux_data.base[x]
254+
match_base_x = blossom_data.aux_data.base[blossom_data.aux_data.match[x]]
255+
# Mark the base as in a blossom
256+
blossom_data.aux_data.in_blossom[base_x] = True
257+
blossom_data.aux_data.in_blossom[match_base_x] = True
258+
259+
# Update the base for all marked vertices
260+
for i in range(len(blossom_data.aux_data.base)):
261+
if blossom_data.aux_data.in_blossom[blossom_data.aux_data.base[i]]:
262+
# Contract to the lowest common ancestor
263+
blossom_data.aux_data.base[i] = blossom_data.lowest_common_ancestor
264+
if not blossom_data.aux_data.in_queue[i]:
265+
# Add to queue if not already present
266+
blossom_data.aux_data.queue.append(i)
267+
blossom_data.aux_data.in_queue[i] = True
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import unittest
2+
3+
from graphs.edmonds_blossom_algorithm import EdmondsBlossomAlgorithm
4+
5+
6+
class EdmondsBlossomAlgorithmTest(unittest.TestCase):
7+
def convert_matching_to_array(self, matching):
8+
"""Helper method to convert a
9+
list of matching pairs into a sorted 2D array.
10+
"""
11+
# Convert the list of pairs into a list of lists
12+
result = [list(pair) for pair in matching]
13+
14+
# Sort each individual pair for consistency
15+
for pair in result:
16+
pair.sort()
17+
18+
# Sort the array of pairs to ensure consistent order
19+
result.sort(key=lambda x: x[0])
20+
return result
21+
22+
def test_case_1(self):
23+
"""Test Case 1: A triangle graph where vertices 0, 1, and 2 form a cycle."""
24+
edges = [[0, 1], [1, 2], [2, 0]]
25+
matching = EdmondsBlossomAlgorithm.maximum_matching(edges, 3)
26+
27+
expected = [[0, 1]]
28+
assert expected == self.convert_matching_to_array(matching)
29+
30+
def test_case_2(self):
31+
"""Test Case 2: A disconnected graph with two components."""
32+
edges = [[0, 1], [1, 2], [3, 4]]
33+
matching = EdmondsBlossomAlgorithm.maximum_matching(edges, 5)
34+
35+
expected = [[0, 1], [3, 4]]
36+
assert expected == self.convert_matching_to_array(matching)
37+
38+
def test_case_3(self):
39+
"""Test Case 3: A cycle graph with an additional edge outside the cycle."""
40+
edges = [[0, 1], [1, 2], [2, 3], [3, 0], [4, 5]]
41+
matching = EdmondsBlossomAlgorithm.maximum_matching(edges, 6)
42+
43+
expected = [[0, 1], [2, 3], [4, 5]]
44+
assert expected == self.convert_matching_to_array(matching)
45+
46+
def test_case_no_matching(self):
47+
"""Test Case 4: A graph with no edges."""
48+
edges = [] # No edges
49+
matching = EdmondsBlossomAlgorithm.maximum_matching(edges, 3)
50+
51+
expected = []
52+
assert expected == self.convert_matching_to_array(matching)
53+
54+
def test_case_large_graph(self):
55+
"""Test Case 5: A complex graph with multiple cycles and extra edges."""
56+
edges = [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 0], [1, 4], [2, 5]]
57+
matching = EdmondsBlossomAlgorithm.maximum_matching(edges, 6)
58+
59+
# Check if the size of the matching is correct (i.e., 3 pairs)
60+
assert len(matching) == 3
61+
62+
# Check that the result contains valid pairs (any order is fine)
63+
possible_matching_1 = [[0, 1], [2, 5], [3, 4]]
64+
possible_matching_2 = [[0, 1], [2, 3], [4, 5]]
65+
result = self.convert_matching_to_array(matching)
66+
67+
# Assert that the result is one of the valid maximum matchings
68+
assert result in (possible_matching_1, possible_matching_2)
69+
70+
71+
if __name__ == "__main__":
72+
unittest.main()

0 commit comments

Comments
 (0)