From e0bc96940ea8e7da60e9095bd55df836955018dc Mon Sep 17 00:00:00 2001 From: Tarun Vishwakarma Date: Mon, 14 Oct 2024 11:39:47 +0530 Subject: [PATCH 01/11] Added edmonds_blossom_algorithm.py. For maximum matching in the graph. --- graphs/edmonds_blossom_algorithm.py | 236 ++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 graphs/edmonds_blossom_algorithm.py diff --git a/graphs/edmonds_blossom_algorithm.py b/graphs/edmonds_blossom_algorithm.py new file mode 100644 index 000000000000..8488f375a520 --- /dev/null +++ b/graphs/edmonds_blossom_algorithm.py @@ -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: + self.aux_data = aux_data + self.u = u + self.v = v + self.lca = lca + From 58536a78844bb7eef7c6bdcfa4585d8bbfaa7474 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 06:13:15 +0000 Subject: [PATCH 02/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- graphs/edmonds_blossom_algorithm.py | 46 ++++++++++++++++++----------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/graphs/edmonds_blossom_algorithm.py b/graphs/edmonds_blossom_algorithm.py index 8488f375a520..c56604688c37 100644 --- a/graphs/edmonds_blossom_algorithm.py +++ b/graphs/edmonds_blossom_algorithm.py @@ -5,8 +5,9 @@ class EdmondsBlossomAlgorithm: @staticmethod - def maximum_matching(edges: list[tuple[int, int]], vertex_count: int) \ - -> list[tuple[int, int]]: + 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. @@ -84,10 +85,16 @@ def maximum_matching(edges: list[tuple[int, int]], vertex_count: int) \ EdmondsBlossomAlgorithm.contract_blossom( BlossomData( BlossomAuxData( - queue, parent, base, in_blossom, - match, in_queue + queue, + parent, + base, + in_blossom, + match, + in_queue, ), - current_vertex, neighbor, base_vertex + current_vertex, + neighbor, + base_vertex, ) ) @@ -100,8 +107,9 @@ def maximum_matching(edges: list[tuple[int, int]], vertex_count: int) \ return matching_result @staticmethod - def update_matching(match: list[int], - parent: list[int], current_vertex: int) -> None: + def update_matching( + match: list[int], parent: list[int], current_vertex: int + ) -> None: """ Updates the matching along the augmenting path found. @@ -160,7 +168,7 @@ def find_base( current_vertex_v = parent[current_vertex_v] @staticmethod - def contract_blossom(blossom_data: 'BlossomData') -> None: + def contract_blossom(blossom_data: "BlossomData") -> None: """ Contracts a blossom in the graph, modifying the base array and marking the vertices involved. @@ -176,9 +184,9 @@ def contract_blossom(blossom_data: 'BlossomData') -> None: 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] - ] + 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[ @@ -188,9 +196,9 @@ def contract_blossom(blossom_data: 'BlossomData') -> None: 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] - ] + 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[ @@ -212,8 +220,13 @@ class BlossomAuxData: """ def __init__( - self, queue: deque, parent: list[int], base: list[int], in_blossom: list[bool], - match: list[int], in_queue: list[bool] + 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 @@ -233,4 +246,3 @@ def __init__(self, aux_data: BlossomAuxData, u: int, v: int, lca: int) -> None: self.u = u self.v = v self.lca = lca - From 050a0af7abbf9e683092b7f03426225ed1f3a4be Mon Sep 17 00:00:00 2001 From: Tarun Vishwakarma Date: Mon, 14 Oct 2024 12:12:44 +0530 Subject: [PATCH 03/11] Added test for edmonds_blossom_algorithm.py in graph/tests/ Resolved the blossom data naming issue. --- graphs/edmonds_blossom_algorithm.py | 21 ++++-- .../tests/test_edmonds_blossom_algorithm.py | 72 +++++++++++++++++++ 2 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 graphs/tests/test_edmonds_blossom_algorithm.py diff --git a/graphs/edmonds_blossom_algorithm.py b/graphs/edmonds_blossom_algorithm.py index 8488f375a520..6ec681d39a2b 100644 --- a/graphs/edmonds_blossom_algorithm.py +++ b/graphs/edmonds_blossom_algorithm.py @@ -173,7 +173,7 @@ def contract_blossom(blossom_data: 'BlossomData') -> None: >>> EdmondsBlossomAlgorithm.contract_blossom(blossom_data) """ # Mark all vertices in the blossom - current_vertex_u = blossom_data.u + 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 @@ -185,7 +185,7 @@ def contract_blossom(blossom_data: 'BlossomData') -> None: blossom_data.aux_data.match[current_vertex_u] ] - current_vertex_v = blossom_data.v + 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 @@ -228,9 +228,18 @@ class BlossomData: BlossomData class with reduced parameters. """ - def __init__(self, aux_data: BlossomAuxData, u: int, v: int, lca: int) -> None: + 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.u = u - self.v = v + self.vertex_u = vertex_u + self.vertex_v = vertex_v self.lca = lca - diff --git a/graphs/tests/test_edmonds_blossom_algorithm.py b/graphs/tests/test_edmonds_blossom_algorithm.py new file mode 100644 index 000000000000..661533d21afd --- /dev/null +++ b/graphs/tests/test_edmonds_blossom_algorithm.py @@ -0,0 +1,72 @@ +import unittest +from collections import deque + +from graphs.edmonds_blossom_algorithm import ( + UNMATCHED, + BlossomAuxData, + BlossomData, + EdmondsBlossomAlgorithm, +) + + +class TestEdmondsBlossomAlgorithm(unittest.TestCase): + + def test_maximum_matching(self): + # Test case: Basic matching in a simple graph + edges = [(0, 1), (1, 2), (2, 3)] + vertex_count = 4 + result = EdmondsBlossomAlgorithm.maximum_matching(edges, vertex_count) + expected_result = [(0, 1), (2, 3)] + assert result == expected_result + + # Test case: Graph with no matching + edges = [] + vertex_count = 4 + result = EdmondsBlossomAlgorithm.maximum_matching(edges, vertex_count) + expected_result = [] + assert result == expected_result + + def test_update_matching(self): + # Test case: Update matching on a simple augmenting path + match = [UNMATCHED, UNMATCHED, UNMATCHED] + parent = [1, 0, UNMATCHED] + current_vertex = 2 + EdmondsBlossomAlgorithm.update_matching(match, parent, current_vertex) + expected_result = [1, 0, UNMATCHED] + assert match == expected_result + + def test_find_base(self): + # Test case: Find base of blossom + base = [0, 1, 2, 3] + parent = [1, 0, UNMATCHED, UNMATCHED] + vertex_u = 2 + vertex_v = 3 + result = EdmondsBlossomAlgorithm.find_base(base, parent, vertex_u, vertex_v) + expected_result = 2 + assert result == expected_result + + def test_contract_blossom(self): + # Test case: Contracting a simple blossom + queue = deque() + parent = [UNMATCHED, UNMATCHED, UNMATCHED] + base = [0, 1, 2] + in_blossom = [False] * 3 + match = [UNMATCHED, UNMATCHED, UNMATCHED] + in_queue = [False] * 3 + aux_data = BlossomAuxData(queue, parent, base, in_blossom, match, in_queue) + blossom_data = BlossomData(aux_data, 0, 1, 2) + + # Contract the blossom + EdmondsBlossomAlgorithm.contract_blossom(blossom_data) + + # Ensure base is updated correctly + assert aux_data.base == [2, 2, 2] + # Check that the queue has the contracted vertices + assert 0 in aux_data.queue + assert 1 in aux_data.queue + assert aux_data.in_queue[0] + assert aux_data.in_queue[1] + + +if __name__ == '__main__': + unittest.main() From e8bec30ae89f3069e3dfe2673398459a0a671aad Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 06:45:49 +0000 Subject: [PATCH 04/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- graphs/edmonds_blossom_algorithm.py | 6 +++--- graphs/tests/test_edmonds_blossom_algorithm.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/graphs/edmonds_blossom_algorithm.py b/graphs/edmonds_blossom_algorithm.py index aa5ce8c708f5..6e51defbe97c 100644 --- a/graphs/edmonds_blossom_algorithm.py +++ b/graphs/edmonds_blossom_algorithm.py @@ -241,8 +241,9 @@ class BlossomData: BlossomData class with reduced parameters. """ - def __init__(self, aux_data: BlossomAuxData, vertex_u: int, - vertex_v: int, lca: int) -> None: + 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. @@ -256,4 +257,3 @@ def __init__(self, aux_data: BlossomAuxData, vertex_u: int, self.vertex_u = vertex_u self.vertex_v = vertex_v self.lca = lca - diff --git a/graphs/tests/test_edmonds_blossom_algorithm.py b/graphs/tests/test_edmonds_blossom_algorithm.py index 661533d21afd..0c44f5bf2576 100644 --- a/graphs/tests/test_edmonds_blossom_algorithm.py +++ b/graphs/tests/test_edmonds_blossom_algorithm.py @@ -10,7 +10,6 @@ class TestEdmondsBlossomAlgorithm(unittest.TestCase): - def test_maximum_matching(self): # Test case: Basic matching in a simple graph edges = [(0, 1), (1, 2), (2, 3)] @@ -68,5 +67,5 @@ def test_contract_blossom(self): assert aux_data.in_queue[1] -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From 41543530780d4fb9287cca6fd643da67786593ed Mon Sep 17 00:00:00 2001 From: Tarun Vishwakarma Date: Mon, 14 Oct 2024 12:22:22 +0530 Subject: [PATCH 05/11] resolved pre commit checks for test_edmonds_blossom_algorithm.py and edmonds_blossom_algorithm.py --- graphs/edmonds_blossom_algorithm.py | 6 +++--- graphs/tests/test_edmonds_blossom_algorithm.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/graphs/edmonds_blossom_algorithm.py b/graphs/edmonds_blossom_algorithm.py index aa5ce8c708f5..6e51defbe97c 100644 --- a/graphs/edmonds_blossom_algorithm.py +++ b/graphs/edmonds_blossom_algorithm.py @@ -241,8 +241,9 @@ class BlossomData: BlossomData class with reduced parameters. """ - def __init__(self, aux_data: BlossomAuxData, vertex_u: int, - vertex_v: int, lca: int) -> None: + 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. @@ -256,4 +257,3 @@ def __init__(self, aux_data: BlossomAuxData, vertex_u: int, self.vertex_u = vertex_u self.vertex_v = vertex_v self.lca = lca - diff --git a/graphs/tests/test_edmonds_blossom_algorithm.py b/graphs/tests/test_edmonds_blossom_algorithm.py index 661533d21afd..0c44f5bf2576 100644 --- a/graphs/tests/test_edmonds_blossom_algorithm.py +++ b/graphs/tests/test_edmonds_blossom_algorithm.py @@ -10,7 +10,6 @@ class TestEdmondsBlossomAlgorithm(unittest.TestCase): - def test_maximum_matching(self): # Test case: Basic matching in a simple graph edges = [(0, 1), (1, 2), (2, 3)] @@ -68,5 +67,5 @@ def test_contract_blossom(self): assert aux_data.in_queue[1] -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From 991a37e9ff4c0b087c761b296a6ac6eadfa5721e Mon Sep 17 00:00:00 2001 From: Tarun Vishwakarma <138651451+TarunVishwakarma1@users.noreply.github.com> Date: Mon, 14 Oct 2024 21:01:01 +0530 Subject: [PATCH 06/11] Changes in the main file and test file as test were failing due to stuck in an infinite loop. --- graphs/edmonds_blossom_algorithm.py | 314 ++++++------------ .../tests/test_edmonds_blossom_algorithm.py | 140 ++++---- 2 files changed, 180 insertions(+), 274 deletions(-) diff --git a/graphs/edmonds_blossom_algorithm.py b/graphs/edmonds_blossom_algorithm.py index 6e51defbe97c..51be59b06de2 100644 --- a/graphs/edmonds_blossom_algorithm.py +++ b/graphs/edmonds_blossom_algorithm.py @@ -1,259 +1,163 @@ -from collections import defaultdict, deque +from collections import deque -UNMATCHED = -1 # Constant to represent unmatched vertices +class BlossomAuxData: + def __init__(self, queue: deque, parent: list[int], base: list[int], + in_blossom: list[bool], match: list[int], in_queue: list[bool]): + self.queue = queue + self.parent = parent + self.base = base + self.in_blossom = in_blossom + self.match = match + self.in_queue = in_queue -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. +class BlossomData: + def __init__(self, aux_data: BlossomAuxData, u: int, v: int, lca: int): + self.aux_data = aux_data + self.u = u + self.v = v + self.lca = lca - :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. +class EdmondsBlossomAlgorithm: + UNMATCHED = -1 # Constant to represent unmatched vertices - >>> EdmondsBlossomAlgorithm.maximum_matching([(0, 1), (1, 2), (2, 3)], 4) - [(0, 1), (2, 3)] - """ - graph: dict[int, list[int]] = defaultdict(list) + @staticmethod + def maximum_matching(edges: list[list[int]], vertex_count: int) -> list[list[int]]: + graph = [[] for _ in range(vertex_count)] # 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)) + for edge in edges: + u, v = edge + graph[u].append(v) + graph[v].append(u) + + # All vertices are initially unmatched + match = [EdmondsBlossomAlgorithm.UNMATCHED] * vertex_count + parent = [EdmondsBlossomAlgorithm.UNMATCHED] * vertex_count + base = list(range(vertex_count)) # Each vertex is its own base initially + # Indicates if a vertex is part of a blossom in_blossom = [False] * vertex_count - in_queue = [False] * vertex_count + in_queue = [False] * vertex_count # Tracks vertices in the BFS queue # Main logic for finding maximum matching - for vertex_u in range(vertex_count): - if match[vertex_u] == UNMATCHED: + for u in range(vertex_count): + if match[u] == EdmondsBlossomAlgorithm.UNMATCHED: # BFS initialization - parent = [UNMATCHED] * vertex_count + parent = [EdmondsBlossomAlgorithm.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 + queue = deque([u]) + in_queue[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: + current = queue.popleft() + for y in graph[current]: + if match[current] == y: + # Skip if we are + # looking at the same edge + # as the current match continue - if base[current_vertex] == base[neighbor]: + if base[current] == base[y]: 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 + if parent[y] == EdmondsBlossomAlgorithm.UNMATCHED: + # Case 1: y is unmatched, we've found an augmenting path + if match[y] == EdmondsBlossomAlgorithm.UNMATCHED: + parent[y] = current augmenting_path_found = True - EdmondsBlossomAlgorithm.update_matching( - match, parent, neighbor - ) + # Augment along this path + (EdmondsBlossomAlgorithm + .update_matching(match, parent, y)) 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 + # Case 2: y is matched, add y's match to the queue + z = match[y] + parent[y] = current + parent[z] = y + if not in_queue[z]: + queue.append(z) + in_queue[z] = True else: - # Case 3: Both current_vertex and neighbor have a parent; + # Case 3: Both current and y 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, - ) - ) + base_u = EdmondsBlossomAlgorithm.find_base(base, + parent, current, y) + if base_u != EdmondsBlossomAlgorithm.UNMATCHED: + EdmondsBlossomAlgorithm.contract_blossom(BlossomData( + BlossomAuxData(queue, + parent, + base, + in_blossom, + match, + in_queue), + current, y, base_u)) # 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])) + for v in range(vertex_count): + if match[v] != EdmondsBlossomAlgorithm.UNMATCHED and v < match[v]: + matching_result.append([v, match[v]]) 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 + def update_matching(match: list[int], parent: list[int], u: int): + while u != EdmondsBlossomAlgorithm.UNMATCHED: + v = parent[u] + next_match = match[v] + match[v] = u + match[u] = v + u = next_match @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 - """ + def find_base(base: list[int], parent: list[int], u: int, v: int) -> int: visited = [False] * len(base) - # Mark ancestors of vertex_u - current_vertex_u = vertex_u + # Mark ancestors of u + current_u = u while True: - current_vertex_u = base[current_vertex_u] - visited[current_vertex_u] = True - if parent[current_vertex_u] == UNMATCHED: + current_u = base[current_u] + visited[current_u] = True + if parent[current_u] == EdmondsBlossomAlgorithm.UNMATCHED: break - current_vertex_u = parent[current_vertex_u] + current_u = parent[current_u] - # Find the common ancestor of vertex_v - current_vertex_v = vertex_v + # Find the common ancestor of v + current_v = 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] + current_v = base[current_v] + if visited[current_v]: + return current_v + current_v = parent[current_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] - ] + def contract_blossom(blossom_data: BlossomData): + for x in range(blossom_data.u, + blossom_data.aux_data.base[blossom_data.u] != blossom_data.lca): + base_x = blossom_data.aux_data.base[x] + match_base_x = blossom_data.aux_data.base[blossom_data.aux_data.match[x]] + blossom_data.aux_data.in_blossom[base_x] = True + blossom_data.aux_data.in_blossom[match_base_x] = True + + for x in range(blossom_data.v, + blossom_data.aux_data.base[blossom_data.v] != blossom_data.lca): + base_x = blossom_data.aux_data.base[x] + match_base_x = blossom_data.aux_data.base[blossom_data.aux_data.match[x]] + blossom_data.aux_data.in_blossom[base_x] = True + blossom_data.aux_data.in_blossom[match_base_x] = True # 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]]: + # Contract to the lowest common ancestor blossom_data.aux_data.base[i] = blossom_data.lca if not blossom_data.aux_data.in_queue[i]: + # Add to queue if not already present 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 diff --git a/graphs/tests/test_edmonds_blossom_algorithm.py b/graphs/tests/test_edmonds_blossom_algorithm.py index 0c44f5bf2576..39d49c3bd27e 100644 --- a/graphs/tests/test_edmonds_blossom_algorithm.py +++ b/graphs/tests/test_edmonds_blossom_algorithm.py @@ -1,71 +1,73 @@ import unittest -from collections import deque - -from graphs.edmonds_blossom_algorithm import ( - UNMATCHED, - BlossomAuxData, - BlossomData, - EdmondsBlossomAlgorithm, -) - - -class TestEdmondsBlossomAlgorithm(unittest.TestCase): - def test_maximum_matching(self): - # Test case: Basic matching in a simple graph - edges = [(0, 1), (1, 2), (2, 3)] - vertex_count = 4 - result = EdmondsBlossomAlgorithm.maximum_matching(edges, vertex_count) - expected_result = [(0, 1), (2, 3)] - assert result == expected_result - - # Test case: Graph with no matching - edges = [] - vertex_count = 4 - result = EdmondsBlossomAlgorithm.maximum_matching(edges, vertex_count) - expected_result = [] - assert result == expected_result - - def test_update_matching(self): - # Test case: Update matching on a simple augmenting path - match = [UNMATCHED, UNMATCHED, UNMATCHED] - parent = [1, 0, UNMATCHED] - current_vertex = 2 - EdmondsBlossomAlgorithm.update_matching(match, parent, current_vertex) - expected_result = [1, 0, UNMATCHED] - assert match == expected_result - - def test_find_base(self): - # Test case: Find base of blossom - base = [0, 1, 2, 3] - parent = [1, 0, UNMATCHED, UNMATCHED] - vertex_u = 2 - vertex_v = 3 - result = EdmondsBlossomAlgorithm.find_base(base, parent, vertex_u, vertex_v) - expected_result = 2 - assert result == expected_result - - def test_contract_blossom(self): - # Test case: Contracting a simple blossom - queue = deque() - parent = [UNMATCHED, UNMATCHED, UNMATCHED] - base = [0, 1, 2] - in_blossom = [False] * 3 - match = [UNMATCHED, UNMATCHED, UNMATCHED] - in_queue = [False] * 3 - aux_data = BlossomAuxData(queue, parent, base, in_blossom, match, in_queue) - blossom_data = BlossomData(aux_data, 0, 1, 2) - - # Contract the blossom - EdmondsBlossomAlgorithm.contract_blossom(blossom_data) - - # Ensure base is updated correctly - assert aux_data.base == [2, 2, 2] - # Check that the queue has the contracted vertices - assert 0 in aux_data.queue - assert 1 in aux_data.queue - assert aux_data.in_queue[0] - assert aux_data.in_queue[1] - - -if __name__ == "__main__": + +from graphs.edmonds_blossom_algorithm import EdmondsBlossomAlgorithm + + +class EdmondsBlossomAlgorithmTest(unittest.TestCase): + + def convert_matching_to_array(self, matching): + """ Helper method to convert a + list of matching pairs into a sorted 2D array. + """ + # Convert the list of pairs into a list of lists + result = [list(pair) for pair in matching] + + # Sort each individual pair for consistency + for pair in result: + pair.sort() + + # Sort the array of pairs to ensure consistent order + result.sort(key=lambda x: x[0]) + return result + + def test_case_1(self): + """ Test Case 1: A triangle graph where vertices 0, 1, and 2 form a cycle. """ + edges = [[0, 1], [1, 2], [2, 0]] + matching = EdmondsBlossomAlgorithm.maximum_matching(edges, 3) + + expected = [[0, 1]] + assert expected == self.convert_matching_to_array(matching) + + def test_case_2(self): + """ Test Case 2: A disconnected graph with two components. """ + edges = [[0, 1], [1, 2], [3, 4]] + matching = EdmondsBlossomAlgorithm.maximum_matching(edges, 5) + + expected = [[0, 1], [3, 4]] + assert expected == self.convert_matching_to_array(matching) + + def test_case_3(self): + """ Test Case 3: A cycle graph with an additional edge outside the cycle. """ + edges = [[0, 1], [1, 2], [2, 3], [3, 0], [4, 5]] + matching = EdmondsBlossomAlgorithm.maximum_matching(edges, 6) + + expected = [[0, 1], [2, 3], [4, 5]] + assert expected == self.convert_matching_to_array(matching) + + def test_case_no_matching(self): + """ Test Case 4: A graph with no edges. """ + edges = [] # No edges + matching = EdmondsBlossomAlgorithm.maximum_matching(edges, 3) + + expected = [] + assert expected == self.convert_matching_to_array(matching) + + def test_case_large_graph(self): + """ Test Case 5: A complex graph with multiple cycles and extra edges. """ + edges = [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 0], [1, 4], [2, 5]] + matching = EdmondsBlossomAlgorithm.maximum_matching(edges, 6) + + # Check if the size of the matching is correct (i.e., 3 pairs) + assert len(matching) == 3 + + # Check that the result contains valid pairs (any order is fine) + possible_matching_1 = [[0, 1], [2, 5], [3, 4]] + possible_matching_2 = [[0, 1], [2, 3], [4, 5]] + result = self.convert_matching_to_array(matching) + + # Assert that the result is one of the valid maximum matchings + assert result in (possible_matching_1, possible_matching_2) + + +if __name__ == '__main__': unittest.main() From d286529fdc8b0f2fd1adc6f8981ad25dafeacb50 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:32:46 +0000 Subject: [PATCH 07/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- graphs/edmonds_blossom_algorithm.py | 60 +++++++++++++------ .../tests/test_edmonds_blossom_algorithm.py | 15 +++-- 2 files changed, 49 insertions(+), 26 deletions(-) diff --git a/graphs/edmonds_blossom_algorithm.py b/graphs/edmonds_blossom_algorithm.py index 51be59b06de2..48bf9f2b52e6 100644 --- a/graphs/edmonds_blossom_algorithm.py +++ b/graphs/edmonds_blossom_algorithm.py @@ -2,8 +2,15 @@ class BlossomAuxData: - def __init__(self, queue: deque, parent: list[int], base: list[int], - in_blossom: list[bool], match: list[int], in_queue: list[bool]): + def __init__( + self, + queue: deque, + parent: list[int], + base: list[int], + in_blossom: list[bool], + match: list[int], + in_queue: list[bool], + ): self.queue = queue self.parent = parent self.base = base @@ -11,6 +18,7 @@ def __init__(self, queue: deque, parent: list[int], base: list[int], self.match = match self.in_queue = in_queue + class BlossomData: def __init__(self, aux_data: BlossomAuxData, u: int, v: int, lca: int): self.aux_data = aux_data @@ -18,6 +26,7 @@ def __init__(self, aux_data: BlossomAuxData, u: int, v: int, lca: int): self.v = v self.lca = lca + class EdmondsBlossomAlgorithm: UNMATCHED = -1 # Constant to represent unmatched vertices @@ -72,8 +81,11 @@ def maximum_matching(edges: list[list[int]], vertex_count: int) -> list[list[int parent[y] = current augmenting_path_found = True # Augment along this path - (EdmondsBlossomAlgorithm - .update_matching(match, parent, y)) + ( + EdmondsBlossomAlgorithm.update_matching( + match, parent, y + ) + ) break # Case 2: y is matched, add y's match to the queue @@ -86,17 +98,25 @@ def maximum_matching(edges: list[list[int]], vertex_count: int) -> list[list[int else: # Case 3: Both current and y have a parent; # check for a cycle/blossom - base_u = EdmondsBlossomAlgorithm.find_base(base, - parent, current, y) + base_u = EdmondsBlossomAlgorithm.find_base( + base, parent, current, y + ) if base_u != EdmondsBlossomAlgorithm.UNMATCHED: - EdmondsBlossomAlgorithm.contract_blossom(BlossomData( - BlossomAuxData(queue, - parent, - base, - in_blossom, - match, - in_queue), - current, y, base_u)) + EdmondsBlossomAlgorithm.contract_blossom( + BlossomData( + BlossomAuxData( + queue, + parent, + base, + in_blossom, + match, + in_queue, + ), + current, + y, + base_u, + ) + ) # Create result list of matched pairs matching_result = [] @@ -138,15 +158,19 @@ def find_base(base: list[int], parent: list[int], u: int, v: int) -> int: @staticmethod def contract_blossom(blossom_data: BlossomData): - for x in range(blossom_data.u, - blossom_data.aux_data.base[blossom_data.u] != blossom_data.lca): + for x in range( + blossom_data.u, + blossom_data.aux_data.base[blossom_data.u] != blossom_data.lca, + ): base_x = blossom_data.aux_data.base[x] match_base_x = blossom_data.aux_data.base[blossom_data.aux_data.match[x]] blossom_data.aux_data.in_blossom[base_x] = True blossom_data.aux_data.in_blossom[match_base_x] = True - for x in range(blossom_data.v, - blossom_data.aux_data.base[blossom_data.v] != blossom_data.lca): + for x in range( + blossom_data.v, + blossom_data.aux_data.base[blossom_data.v] != blossom_data.lca, + ): base_x = blossom_data.aux_data.base[x] match_base_x = blossom_data.aux_data.base[blossom_data.aux_data.match[x]] blossom_data.aux_data.in_blossom[base_x] = True diff --git a/graphs/tests/test_edmonds_blossom_algorithm.py b/graphs/tests/test_edmonds_blossom_algorithm.py index 39d49c3bd27e..6193a502c231 100644 --- a/graphs/tests/test_edmonds_blossom_algorithm.py +++ b/graphs/tests/test_edmonds_blossom_algorithm.py @@ -4,9 +4,8 @@ class EdmondsBlossomAlgorithmTest(unittest.TestCase): - def convert_matching_to_array(self, matching): - """ Helper method to convert a + """Helper method to convert a list of matching pairs into a sorted 2D array. """ # Convert the list of pairs into a list of lists @@ -21,7 +20,7 @@ def convert_matching_to_array(self, matching): return result def test_case_1(self): - """ Test Case 1: A triangle graph where vertices 0, 1, and 2 form a cycle. """ + """Test Case 1: A triangle graph where vertices 0, 1, and 2 form a cycle.""" edges = [[0, 1], [1, 2], [2, 0]] matching = EdmondsBlossomAlgorithm.maximum_matching(edges, 3) @@ -29,7 +28,7 @@ def test_case_1(self): assert expected == self.convert_matching_to_array(matching) def test_case_2(self): - """ Test Case 2: A disconnected graph with two components. """ + """Test Case 2: A disconnected graph with two components.""" edges = [[0, 1], [1, 2], [3, 4]] matching = EdmondsBlossomAlgorithm.maximum_matching(edges, 5) @@ -37,7 +36,7 @@ def test_case_2(self): assert expected == self.convert_matching_to_array(matching) def test_case_3(self): - """ Test Case 3: A cycle graph with an additional edge outside the cycle. """ + """Test Case 3: A cycle graph with an additional edge outside the cycle.""" edges = [[0, 1], [1, 2], [2, 3], [3, 0], [4, 5]] matching = EdmondsBlossomAlgorithm.maximum_matching(edges, 6) @@ -45,7 +44,7 @@ def test_case_3(self): assert expected == self.convert_matching_to_array(matching) def test_case_no_matching(self): - """ Test Case 4: A graph with no edges. """ + """Test Case 4: A graph with no edges.""" edges = [] # No edges matching = EdmondsBlossomAlgorithm.maximum_matching(edges, 3) @@ -53,7 +52,7 @@ def test_case_no_matching(self): assert expected == self.convert_matching_to_array(matching) def test_case_large_graph(self): - """ Test Case 5: A complex graph with multiple cycles and extra edges. """ + """Test Case 5: A complex graph with multiple cycles and extra edges.""" edges = [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 0], [1, 4], [2, 5]] matching = EdmondsBlossomAlgorithm.maximum_matching(edges, 6) @@ -69,5 +68,5 @@ def test_case_large_graph(self): assert result in (possible_matching_1, possible_matching_2) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From 11a3c242ca50f8773b7a5235aa699bbd2f61614c Mon Sep 17 00:00:00 2001 From: Tarun Vishwakarma <138651451+TarunVishwakarma1@users.noreply.github.com> Date: Mon, 14 Oct 2024 21:12:19 +0530 Subject: [PATCH 08/11] Resolved Pre commit errors --- graphs/edmonds_blossom_algorithm.py | 103 ++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 29 deletions(-) diff --git a/graphs/edmonds_blossom_algorithm.py b/graphs/edmonds_blossom_algorithm.py index 51be59b06de2..a133048250c4 100644 --- a/graphs/edmonds_blossom_algorithm.py +++ b/graphs/edmonds_blossom_algorithm.py @@ -2,6 +2,8 @@ class BlossomAuxData: + """Class to hold auxiliary data during the blossom algorithm's execution.""" + def __init__(self, queue: deque, parent: list[int], base: list[int], in_blossom: list[bool], match: list[int], in_queue: list[bool]): self.queue = queue @@ -11,18 +13,33 @@ def __init__(self, queue: deque, parent: list[int], base: list[int], self.match = match self.in_queue = in_queue + class BlossomData: + """Class to encapsulate data related to a blossom in the graph.""" + def __init__(self, aux_data: BlossomAuxData, u: int, v: int, lca: int): self.aux_data = aux_data self.u = u self.v = v self.lca = lca + class EdmondsBlossomAlgorithm: UNMATCHED = -1 # Constant to represent unmatched vertices @staticmethod def maximum_matching(edges: list[list[int]], vertex_count: int) -> list[list[int]]: + """ + Finds the maximum matching in a graph using the Edmonds Blossom Algorithm. + + Args: + edges: A list of edges represented as pairs of vertices. + vertex_count: The total number of vertices in the graph. + + Returns: + A list of matched pairs in the form of a list of lists. + """ + # Create an adjacency list for the graph graph = [[] for _ in range(vertex_count)] # Populate the graph with the edges @@ -35,12 +52,12 @@ def maximum_matching(edges: list[list[int]], vertex_count: int) -> list[list[int match = [EdmondsBlossomAlgorithm.UNMATCHED] * vertex_count parent = [EdmondsBlossomAlgorithm.UNMATCHED] * vertex_count base = list(range(vertex_count)) # Each vertex is its own base initially - # Indicates if a vertex is part of a blossom in_blossom = [False] * vertex_count in_queue = [False] * vertex_count # Tracks vertices in the BFS queue # Main logic for finding maximum matching for u in range(vertex_count): + # Only consider unmatched vertices if match[u] == EdmondsBlossomAlgorithm.UNMATCHED: # BFS initialization parent = [EdmondsBlossomAlgorithm.UNMATCHED] * vertex_count @@ -48,59 +65,58 @@ def maximum_matching(edges: list[list[int]], vertex_count: int) -> list[list[int in_blossom = [False] * vertex_count in_queue = [False] * vertex_count - queue = deque([u]) + queue = deque([u]) # Start BFS from the unmatched vertex in_queue[u] = True augmenting_path_found = False # BFS to find augmenting paths while queue and not augmenting_path_found: - current = queue.popleft() - for y in graph[current]: + current = queue.popleft() # Get the current vertex + for y in graph[current]: # Explore adjacent vertices + # Skip if we're looking at the current match if match[current] == y: - # Skip if we are - # looking at the same edge - # as the current match continue - if base[current] == base[y]: - continue # Avoid self-loops + if base[current] == base[y]: # Avoid self-loops + continue if parent[y] == EdmondsBlossomAlgorithm.UNMATCHED: - # Case 1: y is unmatched, we've found an augmenting path + # Case 1: y is unmatched; we've found an augmenting path if match[y] == EdmondsBlossomAlgorithm.UNMATCHED: - parent[y] = current + parent[y] = current # Update the parent augmenting_path_found = True # Augment along this path - (EdmondsBlossomAlgorithm - .update_matching(match, parent, y)) + EdmondsBlossomAlgorithm.update_matching(match, + parent, + y) break - # Case 2: y is matched, add y's match to the queue + # Case 2: y is matched; add y's match to the queue z = match[y] parent[y] = current parent[z] = y - if not in_queue[z]: + if not in_queue[z]: # If z is not already in the queue queue.append(z) in_queue[z] = True else: # Case 3: Both current and y have a parent; # check for a cycle/blossom base_u = EdmondsBlossomAlgorithm.find_base(base, - parent, current, y) + parent, + current, + y) if base_u != EdmondsBlossomAlgorithm.UNMATCHED: EdmondsBlossomAlgorithm.contract_blossom(BlossomData( - BlossomAuxData(queue, - parent, - base, - in_blossom, - match, - in_queue), + BlossomAuxData(queue, parent, + base, in_blossom, + match, in_queue), current, y, base_u)) # Create result list of matched pairs matching_result = [] for v in range(vertex_count): + # Ensure pairs are unique if match[v] != EdmondsBlossomAlgorithm.UNMATCHED and v < match[v]: matching_result.append([v, match[v]]) @@ -108,22 +124,42 @@ def maximum_matching(edges: list[list[int]], vertex_count: int) -> list[list[int @staticmethod def update_matching(match: list[int], parent: list[int], u: int): + """ + Updates the matching based on the augmenting path found. + + Args: + match: The current match list. + parent: The parent list from BFS traversal. + u: The vertex where the augmenting path ends. + """ while u != EdmondsBlossomAlgorithm.UNMATCHED: - v = parent[u] - next_match = match[v] - match[v] = u - match[u] = v - u = next_match + v = parent[u] # Get the parent vertex + next_match = match[v] # Store the next match + match[v] = u # Update match for v + match[u] = v # Update match for u + u = next_match # Move to the next vertex @staticmethod def find_base(base: list[int], parent: list[int], u: int, v: int) -> int: + """ + Finds the base of the blossom. + + Args: + base: The base array for each vertex. + parent: The parent array from BFS. + u: One endpoint of the blossom. + v: The other endpoint of the blossom. + + Returns: + The lowest common ancestor of u and v in the blossom. + """ visited = [False] * len(base) # Mark ancestors of u current_u = u while True: current_u = base[current_u] - visited[current_u] = True + visited[current_u] = True # Mark this base as visited if parent[current_u] == EdmondsBlossomAlgorithm.UNMATCHED: break current_u = parent[current_u] @@ -132,16 +168,24 @@ def find_base(base: list[int], parent: list[int], u: int, v: int) -> int: current_v = v while True: current_v = base[current_v] - if visited[current_v]: + if visited[current_v]: # Check if we've already visited this base return current_v current_v = parent[current_v] @staticmethod def contract_blossom(blossom_data: BlossomData): + """ + Contracts a blossom found during the matching process. + + Args: + blossom_data: The data related to the blossom to be contracted. + """ + # Mark vertices in the blossom for x in range(blossom_data.u, blossom_data.aux_data.base[blossom_data.u] != blossom_data.lca): base_x = blossom_data.aux_data.base[x] match_base_x = blossom_data.aux_data.base[blossom_data.aux_data.match[x]] + # Mark the base as in a blossom blossom_data.aux_data.in_blossom[base_x] = True blossom_data.aux_data.in_blossom[match_base_x] = True @@ -149,6 +193,7 @@ def contract_blossom(blossom_data: BlossomData): blossom_data.aux_data.base[blossom_data.v] != blossom_data.lca): base_x = blossom_data.aux_data.base[x] match_base_x = blossom_data.aux_data.base[blossom_data.aux_data.match[x]] + # Mark the base as in a blossom blossom_data.aux_data.in_blossom[base_x] = True blossom_data.aux_data.in_blossom[match_base_x] = True From ddf9b59866046d3f6b6eacae29aa95961edb7f77 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:43:53 +0000 Subject: [PATCH 09/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- graphs/edmonds_blossom_algorithm.py | 56 +++++++++++++++++++---------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/graphs/edmonds_blossom_algorithm.py b/graphs/edmonds_blossom_algorithm.py index a133048250c4..f1fd2d77c24e 100644 --- a/graphs/edmonds_blossom_algorithm.py +++ b/graphs/edmonds_blossom_algorithm.py @@ -4,8 +4,15 @@ class BlossomAuxData: """Class to hold auxiliary data during the blossom algorithm's execution.""" - def __init__(self, queue: deque, parent: list[int], base: list[int], - in_blossom: list[bool], match: list[int], in_queue: list[bool]): + def __init__( + self, + queue: deque, + parent: list[int], + base: list[int], + in_blossom: list[bool], + match: list[int], + in_queue: list[bool], + ): self.queue = queue self.parent = parent self.base = base @@ -87,9 +94,9 @@ def maximum_matching(edges: list[list[int]], vertex_count: int) -> list[list[int parent[y] = current # Update the parent augmenting_path_found = True # Augment along this path - EdmondsBlossomAlgorithm.update_matching(match, - parent, - y) + EdmondsBlossomAlgorithm.update_matching( + match, parent, y + ) break # Case 2: y is matched; add y's match to the queue @@ -102,16 +109,25 @@ def maximum_matching(edges: list[list[int]], vertex_count: int) -> list[list[int else: # Case 3: Both current and y have a parent; # check for a cycle/blossom - base_u = EdmondsBlossomAlgorithm.find_base(base, - parent, - current, - y) + base_u = EdmondsBlossomAlgorithm.find_base( + base, parent, current, y + ) if base_u != EdmondsBlossomAlgorithm.UNMATCHED: - EdmondsBlossomAlgorithm.contract_blossom(BlossomData( - BlossomAuxData(queue, parent, - base, in_blossom, - match, in_queue), - current, y, base_u)) + EdmondsBlossomAlgorithm.contract_blossom( + BlossomData( + BlossomAuxData( + queue, + parent, + base, + in_blossom, + match, + in_queue, + ), + current, + y, + base_u, + ) + ) # Create result list of matched pairs matching_result = [] @@ -181,16 +197,20 @@ def contract_blossom(blossom_data: BlossomData): blossom_data: The data related to the blossom to be contracted. """ # Mark vertices in the blossom - for x in range(blossom_data.u, - blossom_data.aux_data.base[blossom_data.u] != blossom_data.lca): + for x in range( + blossom_data.u, + blossom_data.aux_data.base[blossom_data.u] != blossom_data.lca, + ): base_x = blossom_data.aux_data.base[x] match_base_x = blossom_data.aux_data.base[blossom_data.aux_data.match[x]] # Mark the base as in a blossom blossom_data.aux_data.in_blossom[base_x] = True blossom_data.aux_data.in_blossom[match_base_x] = True - for x in range(blossom_data.v, - blossom_data.aux_data.base[blossom_data.v] != blossom_data.lca): + for x in range( + blossom_data.v, + blossom_data.aux_data.base[blossom_data.v] != blossom_data.lca, + ): base_x = blossom_data.aux_data.base[x] match_base_x = blossom_data.aux_data.base[blossom_data.aux_data.match[x]] # Mark the base as in a blossom From 15ba2808ad0d0a0ef7fb66b9da955c5d8a121bc7 Mon Sep 17 00:00:00 2001 From: Tarun Vishwakarma <138651451+TarunVishwakarma1@users.noreply.github.com> Date: Mon, 14 Oct 2024 21:22:06 +0530 Subject: [PATCH 10/11] Resolved per commit checks and unresolved conversations --- graphs/edmonds_blossom_algorithm.py | 133 +++++++++++++++++----------- 1 file changed, 83 insertions(+), 50 deletions(-) diff --git a/graphs/edmonds_blossom_algorithm.py b/graphs/edmonds_blossom_algorithm.py index a133048250c4..b3e52c6e02a9 100644 --- a/graphs/edmonds_blossom_algorithm.py +++ b/graphs/edmonds_blossom_algorithm.py @@ -5,7 +5,19 @@ class BlossomAuxData: """Class to hold auxiliary data during the blossom algorithm's execution.""" def __init__(self, queue: deque, parent: list[int], base: list[int], - in_blossom: list[bool], match: list[int], in_queue: list[bool]): + in_blossom: list[bool], + match: list[int], in_queue: list[bool]) -> None: + """ + Initializes the BlossomAuxData instance. + + Args: + queue: A deque for BFS processing. + parent: List of parent vertices in the augmenting path. + base: List of base vertices for each vertex. + in_blossom: Boolean list indicating if a vertex is in a blossom. + match: List of matched vertices. + in_queue: Boolean list indicating if a vertex is in the queue. + """ self.queue = queue self.parent = parent self.base = base @@ -17,11 +29,22 @@ def __init__(self, queue: deque, parent: list[int], base: list[int], class BlossomData: """Class to encapsulate data related to a blossom in the graph.""" - def __init__(self, aux_data: BlossomAuxData, u: int, v: int, lca: int): + def __init__(self, aux_data: BlossomAuxData, + vertex_u: int, vertex_v: int, + lowest_common_ancestor: int) -> None: + """ + Initializes the BlossomData instance. + + Args: + aux_data: The auxiliary data related to the blossom. + vertex_u: One vertex in the blossom. + vertex_v: The other vertex in the blossom. + lowest_common_ancestor: The lowest common ancestor of vertex_u and vertex_v. + """ self.aux_data = aux_data - self.u = u - self.v = v - self.lca = lca + self.vertex_u = vertex_u + self.vertex_v = vertex_v + self.lowest_common_ancestor = lowest_common_ancestor class EdmondsBlossomAlgorithm: @@ -40,7 +63,7 @@ def maximum_matching(edges: list[list[int]], vertex_count: int) -> list[list[int A list of matched pairs in the form of a list of lists. """ # Create an adjacency list for the graph - graph = [[] for _ in range(vertex_count)] + graph: list[list[int]] = [[] for _ in range(vertex_count)] # Populate the graph with the edges for edge in edges: @@ -49,11 +72,13 @@ def maximum_matching(edges: list[list[int]], vertex_count: int) -> list[list[int graph[v].append(u) # All vertices are initially unmatched - match = [EdmondsBlossomAlgorithm.UNMATCHED] * vertex_count - parent = [EdmondsBlossomAlgorithm.UNMATCHED] * vertex_count - base = list(range(vertex_count)) # Each vertex is its own base initially - in_blossom = [False] * vertex_count - in_queue = [False] * vertex_count # Tracks vertices in the BFS queue + match: list[int] = [EdmondsBlossomAlgorithm.UNMATCHED] * vertex_count + parent: list[int] = [EdmondsBlossomAlgorithm.UNMATCHED] * vertex_count + # Each vertex is its own base initially + base: list[int] = list(range(vertex_count)) + in_blossom: list[bool] = [False] * vertex_count + # Tracks vertices in the BFS queue + in_queue: list[bool] = [False] * vertex_count # Main logic for finding maximum matching for u in range(vertex_count): @@ -82,7 +107,8 @@ def maximum_matching(edges: list[list[int]], vertex_count: int) -> list[list[int continue if parent[y] == EdmondsBlossomAlgorithm.UNMATCHED: - # Case 1: y is unmatched; we've found an augmenting path + # Case 1: y is unmatched; + # we've found an augmenting path if match[y] == EdmondsBlossomAlgorithm.UNMATCHED: parent[y] = current # Update the parent augmenting_path_found = True @@ -92,7 +118,8 @@ def maximum_matching(edges: list[list[int]], vertex_count: int) -> list[list[int y) break - # Case 2: y is matched; add y's match to the queue + # Case 2: y is matched; + # add y's match to the queue z = match[y] parent[y] = current parent[z] = y @@ -102,10 +129,8 @@ def maximum_matching(edges: list[list[int]], vertex_count: int) -> list[list[int else: # Case 3: Both current and y have a parent; # check for a cycle/blossom - base_u = EdmondsBlossomAlgorithm.find_base(base, - parent, - current, - y) + base_u = EdmondsBlossomAlgorithm.find_base(base, parent, + current, y) if base_u != EdmondsBlossomAlgorithm.UNMATCHED: EdmondsBlossomAlgorithm.contract_blossom(BlossomData( BlossomAuxData(queue, parent, @@ -114,66 +139,72 @@ def maximum_matching(edges: list[list[int]], vertex_count: int) -> list[list[int current, y, base_u)) # Create result list of matched pairs - matching_result = [] + matching_result: list[list[int]] = [] for v in range(vertex_count): - # Ensure pairs are unique - if match[v] != EdmondsBlossomAlgorithm.UNMATCHED and v < match[v]: + if (match[v] != EdmondsBlossomAlgorithm.UNMATCHED + and v < match[v]): # Ensure pairs are unique matching_result.append([v, match[v]]) return matching_result @staticmethod - def update_matching(match: list[int], parent: list[int], u: int): + def update_matching(match: list[int], + parent: list[int], + matched_vertex: int) -> None: """ Updates the matching based on the augmenting path found. Args: match: The current match list. parent: The parent list from BFS traversal. - u: The vertex where the augmenting path ends. + matched_vertex: The vertex where the augmenting path ends. """ - while u != EdmondsBlossomAlgorithm.UNMATCHED: - v = parent[u] # Get the parent vertex + while matched_vertex != EdmondsBlossomAlgorithm.UNMATCHED: + v = parent[matched_vertex] # Get the parent vertex next_match = match[v] # Store the next match - match[v] = u # Update match for v - match[u] = v # Update match for u - u = next_match # Move to the next vertex + match[v] = matched_vertex # Update match for v + match[matched_vertex] = v # Update match for matched_vertex + matched_vertex = next_match # Move to the next vertex @staticmethod - def find_base(base: list[int], parent: list[int], u: int, v: int) -> int: + def find_base(base: list[int], + parent: list[int], + vertex_u: int, vertex_v: int) -> int: """ Finds the base of the blossom. Args: base: The base array for each vertex. parent: The parent array from BFS. - u: One endpoint of the blossom. - v: The other endpoint of the blossom. + vertex_u: One endpoint of the blossom. + vertex_v: The other endpoint of the blossom. Returns: - The lowest common ancestor of u and v in the blossom. + The lowest common ancestor of vertex_u and vertex_v in the blossom. """ - visited = [False] * len(base) + visited: list[bool] = [False] * len(base) - # Mark ancestors of u - current_u = u + # Mark ancestors of vertex_u + current_vertex_u = vertex_u while True: - current_u = base[current_u] - visited[current_u] = True # Mark this base as visited - if parent[current_u] == EdmondsBlossomAlgorithm.UNMATCHED: + current_vertex_u = base[current_vertex_u] + # Mark this base as visited + visited[current_vertex_u] = True + if parent[current_vertex_u] == EdmondsBlossomAlgorithm.UNMATCHED: break - current_u = parent[current_u] + current_vertex_u = parent[current_vertex_u] - # Find the common ancestor of v - current_v = v + # Find the common ancestor of vertex_v + current_vertex_v = vertex_v while True: - current_v = base[current_v] - if visited[current_v]: # Check if we've already visited this base - return current_v - current_v = parent[current_v] + current_vertex_v = base[current_vertex_v] + # Check if we've already visited this base + if visited[current_vertex_v]: + return current_vertex_v + current_vertex_v = parent[current_vertex_v] @staticmethod - def contract_blossom(blossom_data: BlossomData): + def contract_blossom(blossom_data: BlossomData) -> None: """ Contracts a blossom found during the matching process. @@ -181,16 +212,18 @@ def contract_blossom(blossom_data: BlossomData): blossom_data: The data related to the blossom to be contracted. """ # Mark vertices in the blossom - for x in range(blossom_data.u, - blossom_data.aux_data.base[blossom_data.u] != blossom_data.lca): + for x in range(blossom_data.vertex_u, + blossom_data.aux_data.base + [blossom_data.vertex_u] != blossom_data.lowest_common_ancestor): base_x = blossom_data.aux_data.base[x] match_base_x = blossom_data.aux_data.base[blossom_data.aux_data.match[x]] # Mark the base as in a blossom blossom_data.aux_data.in_blossom[base_x] = True blossom_data.aux_data.in_blossom[match_base_x] = True - for x in range(blossom_data.v, - blossom_data.aux_data.base[blossom_data.v] != blossom_data.lca): + for x in range(blossom_data.vertex_v, + blossom_data.aux_data.base + [blossom_data.vertex_v] != blossom_data.lowest_common_ancestor): base_x = blossom_data.aux_data.base[x] match_base_x = blossom_data.aux_data.base[blossom_data.aux_data.match[x]] # Mark the base as in a blossom @@ -201,7 +234,7 @@ def contract_blossom(blossom_data: BlossomData): for i in range(len(blossom_data.aux_data.base)): if blossom_data.aux_data.in_blossom[blossom_data.aux_data.base[i]]: # Contract to the lowest common ancestor - blossom_data.aux_data.base[i] = blossom_data.lca + blossom_data.aux_data.base[i] = blossom_data.lowest_common_ancestor if not blossom_data.aux_data.in_queue[i]: # Add to queue if not already present blossom_data.aux_data.queue.append(i) From 17506709f67ab2b177cef6a7947b3a275937179a Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:56:30 +0000 Subject: [PATCH 11/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- graphs/edmonds_blossom_algorithm.py | 55 ++++++++++++++++++----------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/graphs/edmonds_blossom_algorithm.py b/graphs/edmonds_blossom_algorithm.py index 819930b8500b..1f4e941fe4a7 100644 --- a/graphs/edmonds_blossom_algorithm.py +++ b/graphs/edmonds_blossom_algorithm.py @@ -4,9 +4,15 @@ class BlossomAuxData: """Class to hold auxiliary data during the blossom algorithm's execution.""" - def __init__(self, queue: deque, parent: list[int], base: list[int], - in_blossom: list[bool], - match: list[int], in_queue: list[bool]) -> None: + def __init__( + self, + queue: deque, + parent: list[int], + base: list[int], + in_blossom: list[bool], + match: list[int], + in_queue: list[bool], + ) -> None: """ Initializes the BlossomAuxData instance. @@ -29,9 +35,13 @@ def __init__(self, queue: deque, parent: list[int], base: list[int], class BlossomData: """Class to encapsulate data related to a blossom in the graph.""" - def __init__(self, aux_data: BlossomAuxData, - vertex_u: int, vertex_v: int, - lowest_common_ancestor: int) -> None: + def __init__( + self, + aux_data: BlossomAuxData, + vertex_u: int, + vertex_v: int, + lowest_common_ancestor: int, + ) -> None: """ Initializes the BlossomData instance. @@ -152,16 +162,17 @@ def maximum_matching(edges: list[list[int]], vertex_count: int) -> list[list[int # Create result list of matched pairs matching_result: list[list[int]] = [] for v in range(vertex_count): - if (match[v] != EdmondsBlossomAlgorithm.UNMATCHED - and v < match[v]): # Ensure pairs are unique + if ( + match[v] != EdmondsBlossomAlgorithm.UNMATCHED and v < match[v] + ): # Ensure pairs are unique matching_result.append([v, match[v]]) return matching_result @staticmethod - def update_matching(match: list[int], - parent: list[int], - matched_vertex: int) -> None: + def update_matching( + match: list[int], parent: list[int], matched_vertex: int + ) -> None: """ Updates the matching based on the augmenting path found. @@ -178,9 +189,9 @@ def update_matching(match: list[int], matched_vertex = next_match # Move to the next vertex @staticmethod - def find_base(base: list[int], - parent: list[int], - vertex_u: int, vertex_v: int) -> int: + def find_base( + base: list[int], parent: list[int], vertex_u: int, vertex_v: int + ) -> int: """ Finds the base of the blossom. @@ -223,18 +234,22 @@ def contract_blossom(blossom_data: BlossomData) -> None: blossom_data: The data related to the blossom to be contracted. """ # Mark vertices in the blossom - for x in range(blossom_data.vertex_u, - blossom_data.aux_data.base - [blossom_data.vertex_u] != blossom_data.lowest_common_ancestor): + for x in range( + blossom_data.vertex_u, + blossom_data.aux_data.base[blossom_data.vertex_u] + != blossom_data.lowest_common_ancestor, + ): base_x = blossom_data.aux_data.base[x] match_base_x = blossom_data.aux_data.base[blossom_data.aux_data.match[x]] # Mark the base as in a blossom blossom_data.aux_data.in_blossom[base_x] = True blossom_data.aux_data.in_blossom[match_base_x] = True - for x in range(blossom_data.vertex_v, - blossom_data.aux_data.base - [blossom_data.vertex_v] != blossom_data.lowest_common_ancestor): + for x in range( + blossom_data.vertex_v, + blossom_data.aux_data.base[blossom_data.vertex_v] + != blossom_data.lowest_common_ancestor, + ): base_x = blossom_data.aux_data.base[x] match_base_x = blossom_data.aux_data.base[blossom_data.aux_data.match[x]] # Mark the base as in a blossom