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] Changes in the main file and test file as test were failing due to stuck in an infinite loop. --- graphs/edmonds_blossom_algorithm.py | 396 +++++++----------- .../tests/test_edmonds_blossom_algorithm.py | 118 +++--- 2 files changed, 210 insertions(+), 304 deletions(-) diff --git a/graphs/edmonds_blossom_algorithm.py b/graphs/edmonds_blossom_algorithm.py index 6e51defbe..51be59b06 100644 --- a/graphs/edmonds_blossom_algorithm.py +++ b/graphs/edmonds_blossom_algorithm.py @@ -1,233 +1,9 @@ -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 +from collections import deque 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: + 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 @@ -235,25 +11,153 @@ class BlossomAuxData: 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 - """ + def __init__(self, aux_data: BlossomAuxData, u: int, v: int, lca: int): self.aux_data = aux_data - self.vertex_u = vertex_u - self.vertex_v = vertex_v + 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]]: + graph = [[] for _ in range(vertex_count)] + + # Populate the graph with the edges + 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 # Tracks vertices in the BFS queue + + # Main logic for finding maximum matching + for u in range(vertex_count): + if match[u] == EdmondsBlossomAlgorithm.UNMATCHED: + # BFS initialization + parent = [EdmondsBlossomAlgorithm.UNMATCHED] * vertex_count + base = list(range(vertex_count)) + in_blossom = [False] * vertex_count + in_queue = [False] * vertex_count + + queue = deque([u]) + 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]: + 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 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 + # Augment along this path + (EdmondsBlossomAlgorithm + .update_matching(match, parent, y)) + break + + # 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 and y have a parent; + # check for a cycle/blossom + 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 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], 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], u: int, v: int) -> int: + visited = [False] * len(base) + + # Mark ancestors of u + current_u = u + while True: + current_u = base[current_u] + visited[current_u] = True + if parent[current_u] == EdmondsBlossomAlgorithm.UNMATCHED: + break + current_u = parent[current_u] + + # Find the common ancestor of v + current_v = v + while True: + current_v = base[current_v] + if visited[current_v]: + return current_v + current_v = parent[current_v] + + @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): + 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 diff --git a/graphs/tests/test_edmonds_blossom_algorithm.py b/graphs/tests/test_edmonds_blossom_algorithm.py index 0c44f5bf2..39d49c3bd 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, -) +from graphs.edmonds_blossom_algorithm import 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 +class EdmondsBlossomAlgorithmTest(unittest.TestCase): - # Test case: Graph with no matching - edges = [] - vertex_count = 4 - result = EdmondsBlossomAlgorithm.maximum_matching(edges, vertex_count) - expected_result = [] - assert result == expected_result + 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] - 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 + # Sort each individual pair for consistency + for pair in result: + pair.sort() - 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 + # Sort the array of pairs to ensure consistent order + result.sort(key=lambda x: x[0]) + return 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) + 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) - # Contract the blossom - EdmondsBlossomAlgorithm.contract_blossom(blossom_data) + expected = [[0, 1]] + assert expected == self.convert_matching_to_array(matching) - # 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] + 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__": +if __name__ == '__main__': unittest.main()