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