Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added edmonds_blossom_algorithm.py. For maximum matching in the graph. #12043 #12056

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
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
236 changes: 236 additions & 0 deletions graphs/edmonds_blossom_algorithm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
from collections import defaultdict, deque

UNMATCHED = -1 # Constant to represent unmatched vertices


class EdmondsBlossomAlgorithm:
@staticmethod
def maximum_matching(edges: list[tuple[int, int]], vertex_count: int) \
-> list[tuple[int, int]]:
"""
Finds the maximum matching in a general graph using Edmonds' Blossom Algorithm.

:param edges: List of edges in the graph.
:param vertex_count: Number of vertices in the graph.
:return: A list of matched pairs of vertices.

>>> EdmondsBlossomAlgorithm.maximum_matching([(0, 1), (1, 2), (2, 3)], 4)
[(0, 1), (2, 3)]
"""
graph: dict[int, list[int]] = defaultdict(list)

# Populate the graph with the edges
for vertex_u, vertex_v in edges:
graph[vertex_u].append(vertex_v)
graph[vertex_v].append(vertex_u)

# Initial matching array and auxiliary data structures
match = [UNMATCHED] * vertex_count
parent = [UNMATCHED] * vertex_count
base = list(range(vertex_count))
in_blossom = [False] * vertex_count
in_queue = [False] * vertex_count

# Main logic for finding maximum matching
for vertex_u in range(vertex_count):
if match[vertex_u] == UNMATCHED:
# BFS initialization
parent = [UNMATCHED] * vertex_count
base = list(range(vertex_count))
in_blossom = [False] * vertex_count
in_queue = [False] * vertex_count

queue = deque([vertex_u])
in_queue[vertex_u] = True

augmenting_path_found = False

# BFS to find augmenting paths
while queue and not augmenting_path_found:
current_vertex = queue.popleft()
for neighbor in graph[current_vertex]:
if match[current_vertex] == neighbor:
continue

if base[current_vertex] == base[neighbor]:
continue # Avoid self-loops

if parent[neighbor] == UNMATCHED:
# Case 1: neighbor is unmatched,
# we've found an augmenting path
if match[neighbor] == UNMATCHED:
parent[neighbor] = current_vertex
augmenting_path_found = True
EdmondsBlossomAlgorithm.update_matching(
match, parent, neighbor
)
break

# Case 2: neighbor is matched,
# add neighbor's match to the queue
matched_vertex = match[neighbor]
parent[neighbor] = current_vertex
parent[matched_vertex] = neighbor
if not in_queue[matched_vertex]:
queue.append(matched_vertex)
in_queue[matched_vertex] = True
else:
# Case 3: Both current_vertex and neighbor have a parent;
# check for a cycle/blossom
base_vertex = EdmondsBlossomAlgorithm.find_base(
base, parent, current_vertex, neighbor
)
if base_vertex != UNMATCHED:
EdmondsBlossomAlgorithm.contract_blossom(
BlossomData(
BlossomAuxData(
queue, parent, base, in_blossom,
match, in_queue
),
current_vertex, neighbor, base_vertex
)
)

# Create result list of matched pairs
matching_result = []
for vertex in range(vertex_count):
if match[vertex] != UNMATCHED and vertex < match[vertex]:
matching_result.append((vertex, match[vertex]))

return matching_result

@staticmethod
def update_matching(match: list[int],
parent: list[int], current_vertex: int) -> None:
"""
Updates the matching along the augmenting path found.

:param match: The matching array.
:param parent: The parent array used during the BFS.
:param current_vertex: The starting node of the augmenting path.

>>> match = [UNMATCHED, UNMATCHED, UNMATCHED]
>>> parent = [1, 0, UNMATCHED]
>>> EdmondsBlossomAlgorithm.update_matching(match, parent, 2)
>>> match
[1, 0, -1]
"""
while current_vertex != UNMATCHED:
matched_vertex = parent[current_vertex]
next_vertex = match[matched_vertex]
match[matched_vertex] = current_vertex
match[current_vertex] = matched_vertex
current_vertex = next_vertex

@staticmethod
def find_base(
base: list[int], parent: list[int], vertex_u: int, vertex_v: int
) -> int:
"""
Finds the base of a node in the blossom.

:param base: The base array.
:param parent: The parent array.
:param vertex_u: One end of the edge.
:param vertex_v: The other end of the edge.
:return: The base of the node or UNMATCHED.

>>> base = [0, 1, 2, 3]
>>> parent = [1, 0, UNMATCHED, UNMATCHED]
>>> EdmondsBlossomAlgorithm.find_base(base, parent, 2, 3)
2
"""
visited = [False] * len(base)

# Mark ancestors of vertex_u
current_vertex_u = vertex_u
while True:
current_vertex_u = base[current_vertex_u]
visited[current_vertex_u] = True
if parent[current_vertex_u] == UNMATCHED:
break
current_vertex_u = parent[current_vertex_u]

# Find the common ancestor of vertex_v
current_vertex_v = vertex_v
while True:
current_vertex_v = base[current_vertex_v]
if visited[current_vertex_v]:
return current_vertex_v
current_vertex_v = parent[current_vertex_v]

@staticmethod
def contract_blossom(blossom_data: 'BlossomData') -> None:
"""
Contracts a blossom in the graph, modifying the base array
and marking the vertices involved.

:param blossom_data: An object containing the necessary data
to perform the contraction.

>>> aux_data = BlossomAuxData(deque(), [], [], [], [], [])
>>> blossom_data = BlossomData(aux_data, 0, 1, 2)
>>> EdmondsBlossomAlgorithm.contract_blossom(blossom_data)
"""
# Mark all vertices in the blossom
current_vertex_u = blossom_data.u
while blossom_data.aux_data.base[current_vertex_u] != blossom_data.lca:
base_u = blossom_data.aux_data.base[current_vertex_u]
match_base_u = blossom_data.aux_data.base[blossom_data.aux_data.match
[current_vertex_u]
]
blossom_data.aux_data.in_blossom[base_u] = True
blossom_data.aux_data.in_blossom[match_base_u] = True
current_vertex_u = blossom_data.aux_data.parent[
blossom_data.aux_data.match[current_vertex_u]
]

current_vertex_v = blossom_data.v
while blossom_data.aux_data.base[current_vertex_v] != blossom_data.lca:
base_v = blossom_data.aux_data.base[current_vertex_v]
match_base_v = blossom_data.aux_data.base[blossom_data.aux_data.match
[current_vertex_v]
]
blossom_data.aux_data.in_blossom[base_v] = True
blossom_data.aux_data.in_blossom[match_base_v] = True
current_vertex_v = blossom_data.aux_data.parent[
blossom_data.aux_data.match[current_vertex_v]
]

# Update the base for all marked vertices
for i in range(len(blossom_data.aux_data.base)):
if blossom_data.aux_data.in_blossom[blossom_data.aux_data.base[i]]:
blossom_data.aux_data.base[i] = blossom_data.lca
if not blossom_data.aux_data.in_queue[i]:
blossom_data.aux_data.queue.append(i)
blossom_data.aux_data.in_queue[i] = True


class BlossomAuxData:
"""
Auxiliary data class to encapsulate common parameters for the blossom operations.
"""

def __init__(
self, queue: deque, parent: list[int], base: list[int], in_blossom: list[bool],
match: list[int], in_queue: list[bool]
) -> None:
self.queue = queue
self.parent = parent
self.base = base
self.in_blossom = in_blossom
self.match = match
self.in_queue = in_queue


class BlossomData:
"""
BlossomData class with reduced parameters.
"""

def __init__(self, aux_data: BlossomAuxData, u: int, v: int, lca: int) -> None:
TarunVishwakarma1 marked this conversation as resolved.
Show resolved Hide resolved
self.aux_data = aux_data
self.u = u
self.v = v
self.lca = lca

Loading