Python/graphs/edmonds_blossom_algorithm.py

260 lines
9.9 KiB
Python

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.vertex_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.vertex_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, vertex_u: int, vertex_v: int, lca: int
) -> None:
"""
Initialize BlossomData with auxiliary data, two vertices,
and the lowest common ancestor.
:param aux_data: Auxiliary data used in the algorithm
:param vertex_u: First vertex involved in the blossom
:param vertex_v: Second vertex involved in the blossom
:param lca: Lowest common ancestor (base) of the two vertices
"""
self.aux_data = aux_data
self.vertex_u = vertex_u
self.vertex_v = vertex_v
self.lca = lca