diff --git a/python/mwmatching.py b/python/mwmatching.py index 569c398..492fc3d 100644 --- a/python/mwmatching.py +++ b/python/mwmatching.py @@ -34,7 +34,7 @@ def maximum_weight_matching( no effect on the maximum-weight matching. Edges with negative weight are ignored. - This function takes time O(n**3 + n*m*log(n)), + This function takes time O(n * (n + m) * log(n)), where "n" is the number of vertices and "m" is the number of edges. This function uses O(n + m) memory. @@ -544,15 +544,6 @@ class _MatchingContext: # Initially there are no non-trivial blossoms. self.nontrivial_blossom: list[_NonTrivialBlossom] = [] - # Every vertex is contained in exactly one top-level blossom - # (possibly the trivial blossom that contains just that vertex). - # - # "vertex_top_blossom[x]" is the top-level blossom that contains - # vertex "x". - # - # Initially all vertices are trivial top-level blossoms. - self.vertex_top_blossom: list[_Blossom] = self.trivial_blossom.copy() - # "vertex_set_node[x]" represents the vertex "x" inside the # union-find datastructure of its top-level blossom. self.vertex_set_node = [b.vertex_set.insert(i, math.inf) @@ -657,8 +648,8 @@ class _MatchingContext: def edge_slack_2x(self, e: int) -> float: """Return 2 times the slack of the edge with index "e".""" (x, y, w) = self.graph.edges[e] - bx = self.vertex_top_blossom[x] - by = self.vertex_top_blossom[y] + bx = self.vertex_set_node[x].find() + by = self.vertex_set_node[y].find() return self.edge_slack_2x_info(x, y, bx, by, w) # @@ -930,8 +921,9 @@ class _MatchingContext: discovers an augmenting path. In this case it returns an alternating path that starts and ends in an unmatched vertex. - This function takes time O(k) to discover a blossom, where "k" is the - number of sub-blossoms, or time O(n) to discover an augmenting path. + This function takes time O(k*log(n)) to discover a blossom, + where "k" is the number of sub-blossoms, + or time O(n*log(n)) to discover an augmenting path. Returns: Alternating path as an ordered list of edges between top-level @@ -956,7 +948,7 @@ class _MatchingContext: while x != -1 or y != -1: # Check if we found a common ancestor. - bx = self.vertex_top_blossom[x] + bx = self.vertex_set_node[x].find() if bx.marker: first_common = bx break @@ -984,8 +976,8 @@ class _MatchingContext: # If we found a common ancestor, trim the paths so they end there. if first_common is not None: - assert self.vertex_top_blossom[xedges[-1][0]] is first_common - while self.vertex_top_blossom[yedges[-1][0]] is not first_common: + assert self.vertex_set_node[xedges[-1][0]].find() is first_common + while self.vertex_set_node[yedges[-1][0]].find() is not first_common: yedges.pop() # Fuse the two paths. @@ -1009,7 +1001,7 @@ class _MatchingContext: Assign label S to the new blossom. Relabel all T-sub-blossoms as S and add their vertices to the queue. - This function takes total time O(n**2) per stage. + This function takes total time O(n*log(n)) per stage. """ # Check that the path is odd-length. @@ -1017,12 +1009,12 @@ class _MatchingContext: assert len(path.edges) >= 3 # Construct the list of sub-blossoms (current top-level blossoms). - subblossoms = [self.vertex_top_blossom[x] for (x, y) in path.edges] + subblossoms = [self.vertex_set_node[x].find() for (x, y) in path.edges] # Check that the path is cyclic. # Note the path will not always start and end with the same _vertex_, # but it must start and end in the same _blossom_. - subblossoms_next = [self.vertex_top_blossom[y] + subblossoms_next = [self.vertex_set_node[y].find() for (x, y) in path.edges] assert subblossoms[0] == subblossoms_next[-1] assert subblossoms[1:] == subblossoms_next[:-1] @@ -1053,10 +1045,6 @@ class _MatchingContext: for sub in subblossoms: sub.parent = blossom - # Update blossom-membership of all vertices in the new blossom. - for x in blossom.vertices(): - self.vertex_top_blossom[x] = blossom - # Merge union-find structures. blossom.vertex_set.merge([sub.vertex_set for sub in subblossoms]) @@ -1090,7 +1078,7 @@ class _MatchingContext: def expand_t_blossom(self, blossom: _NonTrivialBlossom) -> None: """Expand the specified T-blossom. - This function takes time O(n). + This function takes total time O(n*log(n)) per stage. """ assert blossom.parent is None @@ -1116,12 +1104,6 @@ class _MatchingContext: assert sub.vertex_dual_offset == 0 sub.vertex_dual_offset = vertex_dual_fixup - if isinstance(sub, _NonTrivialBlossom): - for x in sub.vertices(): - self.vertex_top_blossom[x] = sub - else: - self.vertex_top_blossom[sub.base_vertex] = sub - # Insert blossom in delta2_queue if necessary. prio = sub.vertex_set.min_prio() if prio < math.inf: @@ -1139,7 +1121,7 @@ class _MatchingContext: # the alternating tree. assert blossom.tree_edge is not None (x, y) = blossom.tree_edge - sub = self.vertex_top_blossom[y] + sub = self.vertex_set_node[y].find() # Assign label T to that sub-blossom. self.assign_blossom_label_t(sub) @@ -1175,7 +1157,7 @@ class _MatchingContext: def expand_unlabeled_blossom(self, blossom: _NonTrivialBlossom) -> None: """Expand the specified unlabeled blossom. - This function takes time O(n). + This function takes total time O(n*log(n)) per stage. """ assert blossom.parent is None @@ -1196,12 +1178,6 @@ class _MatchingContext: assert sub.vertex_dual_offset == 0 sub.vertex_dual_offset = vertex_dual_offset - if isinstance(sub, _NonTrivialBlossom): - for x in sub.vertices(): - self.vertex_top_blossom[x] = sub - else: - self.vertex_top_blossom[sub.base_vertex] = sub - # Insert blossom in delta2_queue if necessary. prio = sub.vertex_set.min_prio() if prio < math.inf: @@ -1323,7 +1299,7 @@ class _MatchingContext: # an unmatched vertex or a blossom with unmatched base. assert len(path.edges) % 2 == 1 for x in (path.edges[0][0], path.edges[-1][1]): - b = self.vertex_top_blossom[x] + b = self.vertex_set_node[x].find() assert self.vertex_mate[b.base_vertex] == -1 # The augmenting path looks like this: @@ -1341,11 +1317,11 @@ class _MatchingContext: # Augment the non-trivial blossoms on either side of this edge. # No action is necessary for trivial blossoms. - bx = self.vertex_top_blossom[x] + bx = self.vertex_set_node[x].find() if isinstance(bx, _NonTrivialBlossom): self.augment_blossom(bx, self.trivial_blossom[x]) - by = self.vertex_top_blossom[y] + by = self.vertex_set_node[y].find() if isinstance(by, _NonTrivialBlossom): self.augment_blossom(by, self.trivial_blossom[y]) @@ -1372,7 +1348,7 @@ class _MatchingContext: """ # Assign label S to the blossom that contains vertex "x". - bx = self.vertex_top_blossom[x] + bx = self.vertex_set_node[x].find() self.assign_blossom_label_s(bx) self.assign_vertex_label_s(bx) @@ -1388,7 +1364,7 @@ class _MatchingContext: else: # Vertex "x" is matched to T-vertex "y". - by = self.vertex_top_blossom[y] + by = self.vertex_set_node[y].find() assert by.label == _LABEL_T # Attach the blossom that contains "x" to the alternating tree. @@ -1405,14 +1381,14 @@ class _MatchingContext: - "y" is an unlabeled, matched vertex. - There is a tight edge between vertices "x" and "y". """ - assert self.vertex_top_blossom[x].label == _LABEL_S + assert self.vertex_set_node[x].find().label == _LABEL_S - by = self.vertex_top_blossom[y] + by = self.vertex_set_node[y].find() # Expand zero-dual blossoms before assigning label T. while isinstance(by, _NonTrivialBlossom) and (by.dual_var == 0): self.expand_unlabeled_blossom(by) - by = self.vertex_top_blossom[y] + by = self.vertex_set_node[y].find() # Assign label T to the unlabeled blossom. self.assign_blossom_label_t(by) @@ -1446,7 +1422,7 @@ class _MatchingContext: # but not necessarily in the same vertex within that blossom. p = path.edges[0][0] q = path.edges[-1][1] - if self.vertex_top_blossom[p] is self.vertex_top_blossom[q]: + if self.vertex_set_node[p].find() is self.vertex_set_node[q].find(): self.make_blossom(path) return None else: @@ -1475,7 +1451,7 @@ class _MatchingContext: x = self.scan_queue.popleft() # Double-check that "x" is an S-vertex. - bx = self.vertex_top_blossom[x] + bx = self.vertex_set_node[x].find() assert bx.label == _LABEL_S # Scan the edges that are incident on "x". @@ -1489,8 +1465,8 @@ class _MatchingContext: # Note: blossom index of vertex "x" may change during # this loop, so we need to refresh it here. - bx = self.vertex_top_blossom[x] - by = self.vertex_top_blossom[y] + bx = self.vertex_set_node[x].find() + by = self.vertex_set_node[y].find() # Ignore edges that are internal to a blossom. if bx is by: @@ -1589,8 +1565,8 @@ class _MatchingContext: delta3_node = self.delta3_queue.find_min() e = delta3_node.data (x, y, _w) = self.graph.edges[e] - bx = self.vertex_top_blossom[x] - by = self.vertex_top_blossom[y] + bx = self.vertex_set_node[x].find() + by = self.vertex_set_node[y].find() assert (bx.label == _LABEL_S) and (by.label == _LABEL_S) if bx is not by: # Found edge between different top-level S-blossoms. @@ -1634,7 +1610,7 @@ class _MatchingContext: thereby increasing the number of matched edges by 1. If no such path is found, the matching must already be optimal. - This function takes time O(n**2 + m * log(n)). + This function takes time O((n + m) * log(n)). Returns: True if the matching was successfully augmented. @@ -1684,7 +1660,7 @@ class _MatchingContext: # Use the edge from S-vertex to unlabeled vertex that got # unlocked through the delta update. (x, y, _w) = self.graph.edges[delta_edge] - if self.vertex_top_blossom[x].label != _LABEL_S: + if self.vertex_set_node[x].find().label != _LABEL_S: (x, y) = (y, x) self.extend_tree_t(x, y)