|
| 1 | +from collections import deque |
| 2 | + |
| 3 | + |
| 4 | +class BlossomAuxData: |
| 5 | + """Class to hold auxiliary data during the blossom algorithm's execution.""" |
| 6 | + |
| 7 | + def __init__( |
| 8 | + self, |
| 9 | + queue: deque, |
| 10 | + parent: list[int], |
| 11 | + base: list[int], |
| 12 | + in_blossom: list[bool], |
| 13 | + match: list[int], |
| 14 | + in_queue: list[bool], |
| 15 | + ) -> None: |
| 16 | + """ |
| 17 | + Initializes the BlossomAuxData instance. |
| 18 | +
|
| 19 | + Args: |
| 20 | + queue: A deque for BFS processing. |
| 21 | + parent: List of parent vertices in the augmenting path. |
| 22 | + base: List of base vertices for each vertex. |
| 23 | + in_blossom: Boolean list indicating if a vertex is in a blossom. |
| 24 | + match: List of matched vertices. |
| 25 | + in_queue: Boolean list indicating if a vertex is in the queue. |
| 26 | + """ |
| 27 | + self.queue = queue |
| 28 | + self.parent = parent |
| 29 | + self.base = base |
| 30 | + self.in_blossom = in_blossom |
| 31 | + self.match = match |
| 32 | + self.in_queue = in_queue |
| 33 | + |
| 34 | + |
| 35 | +class BlossomData: |
| 36 | + """Class to encapsulate data related to a blossom in the graph.""" |
| 37 | + |
| 38 | + def __init__( |
| 39 | + self, |
| 40 | + aux_data: BlossomAuxData, |
| 41 | + vertex_u: int, |
| 42 | + vertex_v: int, |
| 43 | + lowest_common_ancestor: int, |
| 44 | + ) -> None: |
| 45 | + """ |
| 46 | + Initializes the BlossomData instance. |
| 47 | +
|
| 48 | + Args: |
| 49 | + aux_data: The auxiliary data related to the blossom. |
| 50 | + vertex_u: One vertex in the blossom. |
| 51 | + vertex_v: The other vertex in the blossom. |
| 52 | + lowest_common_ancestor: The lowest common ancestor of vertex_u and vertex_v. |
| 53 | + """ |
| 54 | + self.aux_data = aux_data |
| 55 | + self.vertex_u = vertex_u |
| 56 | + self.vertex_v = vertex_v |
| 57 | + self.lowest_common_ancestor = lowest_common_ancestor |
| 58 | + |
| 59 | + |
| 60 | +class EdmondsBlossomAlgorithm: |
| 61 | + UNMATCHED = -1 # Constant to represent unmatched vertices |
| 62 | + |
| 63 | + @staticmethod |
| 64 | + def maximum_matching(edges: list[list[int]], vertex_count: int) -> list[list[int]]: |
| 65 | + """ |
| 66 | + Finds the maximum matching in a graph using the Edmonds Blossom Algorithm. |
| 67 | +
|
| 68 | + Args: |
| 69 | + edges: A list of edges represented as pairs of vertices. |
| 70 | + vertex_count: The total number of vertices in the graph. |
| 71 | +
|
| 72 | + Returns: |
| 73 | + A list of matched pairs in the form of a list of lists. |
| 74 | + """ |
| 75 | + # Create an adjacency list for the graph |
| 76 | + graph: list[list[int]] = [[] for _ in range(vertex_count)] |
| 77 | + |
| 78 | + # Populate the graph with the edges |
| 79 | + for edge in edges: |
| 80 | + u, v = edge |
| 81 | + graph[u].append(v) |
| 82 | + graph[v].append(u) |
| 83 | + |
| 84 | + # All vertices are initially unmatched |
| 85 | + match: list[int] = [EdmondsBlossomAlgorithm.UNMATCHED] * vertex_count |
| 86 | + parent: list[int] = [EdmondsBlossomAlgorithm.UNMATCHED] * vertex_count |
| 87 | + # Each vertex is its own base initially |
| 88 | + base: list[int] = list(range(vertex_count)) |
| 89 | + in_blossom: list[bool] = [False] * vertex_count |
| 90 | + # Tracks vertices in the BFS queue |
| 91 | + in_queue: list[bool] = [False] * vertex_count |
| 92 | + |
| 93 | + # Main logic for finding maximum matching |
| 94 | + for u in range(vertex_count): |
| 95 | + # Only consider unmatched vertices |
| 96 | + if match[u] == EdmondsBlossomAlgorithm.UNMATCHED: |
| 97 | + # BFS initialization |
| 98 | + parent = [EdmondsBlossomAlgorithm.UNMATCHED] * vertex_count |
| 99 | + base = list(range(vertex_count)) |
| 100 | + in_blossom = [False] * vertex_count |
| 101 | + in_queue = [False] * vertex_count |
| 102 | + |
| 103 | + queue = deque([u]) # Start BFS from the unmatched vertex |
| 104 | + in_queue[u] = True |
| 105 | + |
| 106 | + augmenting_path_found = False |
| 107 | + |
| 108 | + # BFS to find augmenting paths |
| 109 | + while queue and not augmenting_path_found: |
| 110 | + current = queue.popleft() # Get the current vertex |
| 111 | + for y in graph[current]: # Explore adjacent vertices |
| 112 | + # Skip if we're looking at the current match |
| 113 | + if match[current] == y: |
| 114 | + continue |
| 115 | + |
| 116 | + if base[current] == base[y]: # Avoid self-loops |
| 117 | + continue |
| 118 | + |
| 119 | + if parent[y] == EdmondsBlossomAlgorithm.UNMATCHED: |
| 120 | + # Case 1: y is unmatched; |
| 121 | + # we've found an augmenting path |
| 122 | + if match[y] == EdmondsBlossomAlgorithm.UNMATCHED: |
| 123 | + parent[y] = current # Update the parent |
| 124 | + augmenting_path_found = True |
| 125 | + # Augment along this path |
| 126 | + EdmondsBlossomAlgorithm.update_matching( |
| 127 | + match, parent, y |
| 128 | + ) |
| 129 | + break |
| 130 | + |
| 131 | + # Case 2: y is matched; |
| 132 | + # add y's match to the queue |
| 133 | + z = match[y] |
| 134 | + parent[y] = current |
| 135 | + parent[z] = y |
| 136 | + if not in_queue[z]: # If z is not already in the queue |
| 137 | + queue.append(z) |
| 138 | + in_queue[z] = True |
| 139 | + else: |
| 140 | + # Case 3: Both current and y have a parent; |
| 141 | + # check for a cycle/blossom |
| 142 | + base_u = EdmondsBlossomAlgorithm.find_base( |
| 143 | + base, parent, current, y |
| 144 | + ) |
| 145 | + if base_u != EdmondsBlossomAlgorithm.UNMATCHED: |
| 146 | + EdmondsBlossomAlgorithm.contract_blossom( |
| 147 | + BlossomData( |
| 148 | + BlossomAuxData( |
| 149 | + queue, |
| 150 | + parent, |
| 151 | + base, |
| 152 | + in_blossom, |
| 153 | + match, |
| 154 | + in_queue, |
| 155 | + ), |
| 156 | + current, |
| 157 | + y, |
| 158 | + base_u, |
| 159 | + ) |
| 160 | + ) |
| 161 | + |
| 162 | + # Create result list of matched pairs |
| 163 | + matching_result: list[list[int]] = [] |
| 164 | + for v in range(vertex_count): |
| 165 | + if ( |
| 166 | + match[v] != EdmondsBlossomAlgorithm.UNMATCHED and v < match[v] |
| 167 | + ): # Ensure pairs are unique |
| 168 | + matching_result.append([v, match[v]]) |
| 169 | + |
| 170 | + return matching_result |
| 171 | + |
| 172 | + @staticmethod |
| 173 | + def update_matching( |
| 174 | + match: list[int], parent: list[int], matched_vertex: int |
| 175 | + ) -> None: |
| 176 | + """ |
| 177 | + Updates the matching based on the augmenting path found. |
| 178 | +
|
| 179 | + Args: |
| 180 | + match: The current match list. |
| 181 | + parent: The parent list from BFS traversal. |
| 182 | + matched_vertex: The vertex where the augmenting path ends. |
| 183 | + """ |
| 184 | + while matched_vertex != EdmondsBlossomAlgorithm.UNMATCHED: |
| 185 | + v = parent[matched_vertex] # Get the parent vertex |
| 186 | + next_match = match[v] # Store the next match |
| 187 | + match[v] = matched_vertex # Update match for v |
| 188 | + match[matched_vertex] = v # Update match for matched_vertex |
| 189 | + matched_vertex = next_match # Move to the next vertex |
| 190 | + |
| 191 | + @staticmethod |
| 192 | + def find_base( |
| 193 | + base: list[int], parent: list[int], vertex_u: int, vertex_v: int |
| 194 | + ) -> int: |
| 195 | + """ |
| 196 | + Finds the base of the blossom. |
| 197 | +
|
| 198 | + Args: |
| 199 | + base: The base array for each vertex. |
| 200 | + parent: The parent array from BFS. |
| 201 | + vertex_u: One endpoint of the blossom. |
| 202 | + vertex_v: The other endpoint of the blossom. |
| 203 | +
|
| 204 | + Returns: |
| 205 | + The lowest common ancestor of vertex_u and vertex_v in the blossom. |
| 206 | + """ |
| 207 | + visited: list[bool] = [False] * len(base) |
| 208 | + |
| 209 | + # Mark ancestors of vertex_u |
| 210 | + current_vertex_u = vertex_u |
| 211 | + while True: |
| 212 | + current_vertex_u = base[current_vertex_u] |
| 213 | + # Mark this base as visited |
| 214 | + visited[current_vertex_u] = True |
| 215 | + if parent[current_vertex_u] == EdmondsBlossomAlgorithm.UNMATCHED: |
| 216 | + break |
| 217 | + current_vertex_u = parent[current_vertex_u] |
| 218 | + |
| 219 | + # Find the common ancestor of vertex_v |
| 220 | + current_vertex_v = vertex_v |
| 221 | + while True: |
| 222 | + current_vertex_v = base[current_vertex_v] |
| 223 | + # Check if we've already visited this base |
| 224 | + if visited[current_vertex_v]: |
| 225 | + return current_vertex_v |
| 226 | + current_vertex_v = parent[current_vertex_v] |
| 227 | + |
| 228 | + @staticmethod |
| 229 | + def contract_blossom(blossom_data: BlossomData) -> None: |
| 230 | + """ |
| 231 | + Contracts a blossom found during the matching process. |
| 232 | +
|
| 233 | + Args: |
| 234 | + blossom_data: The data related to the blossom to be contracted. |
| 235 | + """ |
| 236 | + # Mark vertices in the blossom |
| 237 | + for x in range( |
| 238 | + blossom_data.vertex_u, |
| 239 | + blossom_data.aux_data.base[blossom_data.vertex_u] |
| 240 | + != blossom_data.lowest_common_ancestor, |
| 241 | + ): |
| 242 | + base_x = blossom_data.aux_data.base[x] |
| 243 | + match_base_x = blossom_data.aux_data.base[blossom_data.aux_data.match[x]] |
| 244 | + # Mark the base as in a blossom |
| 245 | + blossom_data.aux_data.in_blossom[base_x] = True |
| 246 | + blossom_data.aux_data.in_blossom[match_base_x] = True |
| 247 | + |
| 248 | + for x in range( |
| 249 | + blossom_data.vertex_v, |
| 250 | + blossom_data.aux_data.base[blossom_data.vertex_v] |
| 251 | + != blossom_data.lowest_common_ancestor, |
| 252 | + ): |
| 253 | + base_x = blossom_data.aux_data.base[x] |
| 254 | + match_base_x = blossom_data.aux_data.base[blossom_data.aux_data.match[x]] |
| 255 | + # Mark the base as in a blossom |
| 256 | + blossom_data.aux_data.in_blossom[base_x] = True |
| 257 | + blossom_data.aux_data.in_blossom[match_base_x] = True |
| 258 | + |
| 259 | + # Update the base for all marked vertices |
| 260 | + for i in range(len(blossom_data.aux_data.base)): |
| 261 | + if blossom_data.aux_data.in_blossom[blossom_data.aux_data.base[i]]: |
| 262 | + # Contract to the lowest common ancestor |
| 263 | + blossom_data.aux_data.base[i] = blossom_data.lowest_common_ancestor |
| 264 | + if not blossom_data.aux_data.in_queue[i]: |
| 265 | + # Add to queue if not already present |
| 266 | + blossom_data.aux_data.queue.append(i) |
| 267 | + blossom_data.aux_data.in_queue[i] = True |
0 commit comments