diff --git a/python/mwmatching.py b/python/mwmatching.py index 740086a..31c527b 100644 --- a/python/mwmatching.py +++ b/python/mwmatching.py @@ -78,7 +78,7 @@ def maximum_weight_matching( # of matched edges by 1. # # This loop runs through at most (n/2 + 1) iterations. - # Each iteration takes time O(n**2). + # Each iteration takes time O((n + m) * log(n)). while ctx.run_stage(): pass @@ -554,6 +554,8 @@ class _MatchingContext: # "vertex_set_node[x]" represents the vertex "x" inside the # union-find datastructure of its top-level blossom. + # + # Initially, each vertex belongs to its owwn trivial top-level blossom. self.vertex_set_node = [b.vertex_set.insert(i, math.inf) for (i, b) in enumerate(self.trivial_blossom)] @@ -587,7 +589,7 @@ class _MatchingContext: self.delta_sum_2x: float = 0 # Queue containing unlabeled top-level blossoms that have an edge to - # an S-blossom. The priority of a blossom is 2 times the least slack + # an S-blossom. The priority of a blossom is 2 times its least slack # to an S blossom, plus 2 times the running sum of delta steps. self.delta2_queue: PriorityQueue[_Blossom] = PriorityQueue() @@ -623,11 +625,15 @@ class _MatchingContext: del blossom.vertex_set blossom.tree_blossoms = None + # + # Least-slack edge tracking: + # + def edge_pseudo_slack_2x(self, e: int) -> float: """Return 2 times the pseudo-slack of the specified edge. The pseudo-slack of an edge is related to its true slack, but - distorted in a way that makes it invariant under delta steps. + adjusted in a way that makes it invariant under delta steps. If the edge connects two S-vertices in different top-level blossoms, the true slack is the pseudo-slack minus 2 times the running sum @@ -641,47 +647,33 @@ class _MatchingContext: (x, y, w) = self.graph.edges[e] return self.vertex_dual_2x[x] + self.vertex_dual_2x[y] - 2 * w - # - # Least-slack edge tracking: - # - # To calculate delta steps, the matching algorithm needs to find - # - the least-slack edge between any S-vertex and an unlabeled vertex; - # - the least-slack edge between any pair of top-level S-blossoms. - # - # For each unlabeled vertex and each T-vertex, we keep track of the - # least-slack edge to any S-vertex. Tracking for unlabeled vertices - # serves to provide the least-slack edge for the delta step. - # Tracking for T-vertices is done because such vertices can turn into - # unlabeled vertices if they are part of a T-blossom that gets expanded. - # - # Note: For a given vertex or blossom, the identity of the least-slack - # edge to any S-blossom remains unchanged during a delta step. - # Although the delta step changes edge slacks, it changes the slack - # of every edge to an S-vertex by the same amount. Therefore the edge - # that had least slack before the delta step, will still have least slack - # after the delta step. - # + def delta2_add_edge(self, e: int, y: int, by: _Blossom) -> None: + """Add edge "e" for delta2 tracking. - # TODO -- rename function, maybe refactor - def lset_add_vertex_edge(self, y: int, by: _Blossom, e: int) -> None: - """Add edge "e" from an S-vertex to unlabeled vertex or T-vertex "y". + Edge "e" connects an S-vertex to a T-vertex or unlabeled vertex "y". This function takes time O(log(n)). """ + prio = self.edge_pseudo_slack_2x(e) improved = (self.vertex_sedge_queue[y].empty() or (self.vertex_sedge_queue[y].find_min().prio > prio)) + # Insert edge in the S-edge queue of vertex "y". assert self.vertex_sedge_node[e] is None self.vertex_sedge_node[e] = self.vertex_sedge_queue[y].insert(prio, e) + # Continue if the new edge becomes the least-slack S-edge for "y". if not improved: return + # Update the priority of "y" in its UnionFindQueue. prev_min = by.vertex_set.min_prio() self.vertex_set_node[y].set_prio(prio) + # If the blossom is unlabeled and the new edge becomes its least-slack + # S-edge, insert or update the blossom in the global delta2 queue. if (by.label == _LABEL_NONE) and (prio < prev_min): prio += by.vertex_dual_offset if by.delta2_node is None: @@ -689,20 +681,99 @@ class _MatchingContext: elif prio < by.delta2_node.prio: self.delta2_queue.decrease_prio(by.delta2_node, prio) - # TODO -- rename function, maybe refactor - def lset_get_best_vertex_edge(self) -> tuple[int, float]: - """Return the index and slack of the least-slack edge between - any S-vertex and unlabeled vertex. + def delta2_remove_edge(self, e: int, y: int, by: _Blossom) -> None: + """Remove edge "e" from delta2 tracking. + + This function is called if an S-vertex becomes unlabeled, + and edge "e" connects that vertex to vertex "y" which is a T-vertex + or unlabeled vertex. + + This function takes time O(log(n)). + """ + vertex_sedge_node = self.vertex_sedge_node[e] + if vertex_sedge_node is not None: + # Delete edge from the S-edge queue of vertex "y". + vertex_sedge_queue = self.vertex_sedge_queue[y] + vertex_sedge_queue.delete(vertex_sedge_node) + self.vertex_sedge_node[e] = None + + if vertex_sedge_queue.empty(): + prio = math.inf + else: + prio = vertex_sedge_queue.find_min().prio + + # If necessary, update the priority of "y" in its UnionFindQueue. + if prio > self.vertex_set_node[y].prio: + self.vertex_set_node[y].set_prio(prio) + if by.label == _LABEL_NONE: + # Update or delete the blossom in the global delta2 queue. + assert by.delta2_node is not None + prio = by.vertex_set.min_prio() + if prio < math.inf: + prio += by.vertex_dual_offset + if prio > by.delta2_node.prio: + self.delta2_queue.increase_prio( + by.delta2_node, prio) + else: + self.delta2_queue.delete(by.delta2_node) + by.delta2_node = None + + def delta2_enable_blossom(self, blossom: _Blossom) -> None: + """Enable delta2 tracking for "blossom". + + This function is called when a blossom becomes an unlabeled top-level + blossom. If the blossom has at least one edge to an S-vertex, + the blossom will be inserted in the global delta2 queue. + + This function takes time O(log(n)). + """ + assert blossom.delta2_node is None + prio = blossom.vertex_set.min_prio() + if prio < math.inf: + prio += blossom.vertex_dual_offset + blossom.delta2_node = self.delta2_queue.insert(prio, blossom) + + def delta2_disable_blossom(self, blossom: _Blossom) -> None: + """Disable delta2 tracking for "blossom". + + The blossom will be removed from the global delta2 queue. + This function is called when a blossom stops being an unlabeled + top-level blossom. + + This function takes time O(log(n)). + """ + if blossom.delta2_node is not None: + self.delta2_queue.delete(blossom.delta2_node) + blossom.delta2_node = None + + def delta2_clear_vertex(self, x: int) -> None: + """Clear delta2 tracking for vertex "x". + + This function is called when "x" becomes an S-vertex. + It is assumed that the blossom containing "x" has already been + disabled for delta2 tracking. + + This function takes time O(k * log(n)), + where "k" is the number of edges incident on "x". + """ + self.vertex_sedge_queue[x].clear() + for e in self.graph.adjacent_edges[x]: + self.vertex_sedge_node[e] = None + self.vertex_set_node[x].set_prio(math.inf) + + def delta2_get_min_edge(self) -> tuple[int, float]: + """Find the least-slack edge between any S-vertex and any unlabeled + vertex. This function takes time O(log(n)). Returns: - Tuple (edge_index, slack_2x) if there is a least-slack edge, - or (-1, 0) if there is no suitable edge. + Tuple (edge_index, slack_2x) if there is an S-to-unlabeled edge, + or (-1, Inf) if there is no such edge. """ if self.delta2_queue.empty(): - return (-1, 0) + return (-1, math.inf) delta2_node = self.delta2_queue.find_min() blossom = delta2_node.data @@ -716,6 +787,74 @@ class _MatchingContext: return (e, slack_2x) + def delta3_add_edge(self, e: int) -> None: + """Add edge "e" for delta3 tracking. + + This function is called if a vertex becomes an S-vertex and edge "e" + connects it to an S-vertex in a different top-level blossom. + + This function takes time O(log(n)). + """ + # The edge may already be in the delta3 queue, if it was previously + # discovered in the opposite direction. + if self.delta3_node[e] is None: + # Priority is edge slack plus 2 times the running sum of + # delta steps. + prio_2x = self.edge_pseudo_slack_2x(e) + if self.graph.integer_weights: + # If all edge weights are integers, the slack of + # any edge between S-vertices is also an integer. + assert prio_2x % 2 == 0 + prio = prio_2x // 2 + else: + prio = prio_2x / 2 + self.delta3_node[e] = self.delta3_queue.insert(prio, e) + + def delta3_remove_edge(self, e: int) -> None: + """Remove edge "e" from delta3 tracking. + + This function is called if a former S-vertex becomes unlabeled, + and edge "e" connects it to another S-vertex. + + This function takes time O(log(n)). + """ + delta3_node = self.delta3_node[e] + if delta3_node is not None: + self.delta3_queue.delete(delta3_node) + self.delta3_node[e] = None + + def delta3_get_min_edge(self) -> tuple[int, float]: + """Find the least-slack edge between any pair of S-vertices in + different top-level blossoms. + + This function takes time O(1 + k * log(n)), + where "k" is the number of intra-blossom edges removed from the queue. + + Returns: + Tuple (edge_index, slack) if there is an S-to-S edge, + or (-1, Inf) if there is no suitable edge. + """ + while not self.delta3_queue.empty(): + delta3_node = self.delta3_queue.find_min() + e = delta3_node.data + (x, y, _w) = self.graph.edges[e] + 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: + slack = delta3_node.prio - self.delta_sum_2x + return (e, slack) + + # Reject edges between vertices within the same top-level blossom. + # Although intra-blossom edges are never inserted into the queue, + # existing edges in the queue may become intra-blossom when + # a new blossom is formed. + self.delta3_queue.delete(delta3_node) + self.delta3_node[e] = None + + # If the queue is empty, no suitable edge exists. + return (-1, math.inf) + # # General support routines: # @@ -734,10 +873,7 @@ class _MatchingContext: assert blossom.label == _LABEL_NONE blossom.label = _LABEL_S - # Delete blossom from delta2 queue. - if blossom.delta2_node is not None: - self.delta2_queue.delete(blossom.delta2_node) - blossom.delta2_node = None + self.delta2_disable_blossom(blossom) # Prepare for lazy updating of S-blossom dual variable. if isinstance(blossom, _NonTrivialBlossom): @@ -765,14 +901,7 @@ class _MatchingContext: blossom.vertex_dual_offset = 0 for x in vertices: self.vertex_dual_2x[x] += vertex_dual_fixup - - # Clean up tracking of edges from vertex "x" to S-vertices. - # We maintain that tracking only for unlabeled vertices and - # T-vertices. - self.vertex_sedge_queue[x].clear() - for e in self.graph.adjacent_edges[x]: - self.vertex_sedge_node[e] = None - self.vertex_set_node[x].set_prio(math.inf) + self.delta2_clear_vertex(x) def assign_blossom_label_t(self, blossom: _Blossom) -> None: """Assign label T to an unlabeled top-level blossom.""" @@ -781,10 +910,7 @@ class _MatchingContext: assert blossom.label == _LABEL_NONE blossom.label = _LABEL_T - # Delete blossom from delta2 queue. - if blossom.delta2_node is not None: - self.delta2_queue.delete(blossom.delta2_node) - blossom.delta2_node = None + self.delta2_disable_blossom(blossom) if isinstance(blossom, _NonTrivialBlossom): @@ -838,47 +964,17 @@ class _MatchingContext: (p, q, _w) = edges[e] y = p if p != x else q - # If this edge was in the delta3_queue, remove it since - # this is no longer an edge between S-vertices. - delta3_node = self.delta3_node[e] - if delta3_node is not None: - self.delta3_queue.delete(delta3_node) - self.delta3_node[e] = None + self.delta3_remove_edge(e) by = self.vertex_set_node[y].find() if by.label == _LABEL_S: # This is an edge between "x" and an S-vertex. # Add this edge to "vertex_sedge_queue[x]". # Update delta2 tracking accordingly. - self.lset_add_vertex_edge(x, bx, e) + self.delta2_add_edge(e, x, bx) else: - # This is no longer an edge between "y" and an S-vertex. - # Remove this edge from "vertex_sedge_queue[y]". - # Update delta2 tracking accordingly. - # TODO -- untangle this mess - vertex_sedge_node = self.vertex_sedge_node[e] - if vertex_sedge_node is not None: - vertex_sedge_queue = self.vertex_sedge_queue[y] - vertex_sedge_queue.delete(vertex_sedge_node) - self.vertex_sedge_node[e] = None - if vertex_sedge_queue.empty(): - prio = math.inf - else: - prio = vertex_sedge_queue.find_min().prio - if prio > self.vertex_set_node[y].prio: - self.vertex_set_node[y].set_prio(prio) - if by.label == _LABEL_NONE: - assert by.delta2_node is not None - prio = by.vertex_set.min_prio() - if prio < math.inf: - prio += by.vertex_dual_offset - if prio > by.delta2_node.prio: - self.delta2_queue.increase_prio( - by.delta2_node, prio) - else: - self.delta2_queue.delete(by.delta2_node) - by.delta2_node = None + self.delta2_remove_edge(e, y, by) def reset_blossom_label(self, blossom: _Blossom) -> None: """Remove blossom label.""" @@ -907,14 +1003,7 @@ class _MatchingContext: elif blossom.label == _LABEL_T: self.remove_blossom_label_t(blossom) - - # Since the blossom is now unlabeled, insert it in delta2_queue - # if it has at least one edge to an S-vertex. - assert blossom.delta2_node is None - prio = blossom.vertex_set.min_prio() - if prio < math.inf: - prio += blossom.vertex_dual_offset - blossom.delta2_node = self.delta2_queue.insert(prio, blossom) + self.delta2_enable_blossom(blossom) def _check_alternating_tree_consistency(self) -> None: """TODO -- remove this function, only for debugging""" @@ -1161,12 +1250,7 @@ class _MatchingContext: assert sub.vertex_dual_offset == 0 sub.vertex_dual_offset = vertex_dual_fixup - # Insert blossom in delta2_queue if necessary. - prio = sub.vertex_set.min_prio() - if prio < math.inf: - assert sub.delta2_node is None - prio += sub.vertex_dual_offset - sub.delta2_node = self.delta2_queue.insert(prio, sub) + self.delta2_enable_blossom(sub) # The expanding blossom was part of an alternating tree, linked to # a parent node in the tree via one of its subblossoms, and linked to @@ -1224,10 +1308,7 @@ class _MatchingContext: assert blossom.parent is None assert blossom.label == _LABEL_NONE - # Remove blossom from delta2 heap. - assert blossom.delta2_node is not None - self.delta2_queue.delete(blossom.delta2_node) - blossom.delta2_node = None + self.delta2_disable_blossom(blossom) # Split union-find structure. blossom.vertex_set.split() @@ -1244,12 +1325,7 @@ class _MatchingContext: assert sub.vertex_dual_offset == 0 sub.vertex_dual_offset = vertex_dual_offset - # Insert blossom in delta2_queue if necessary. - prio = sub.vertex_set.min_prio() - if prio < math.inf: - assert sub.delta2_node is None - prio += sub.vertex_dual_offset - sub.delta2_node = self.delta2_queue.insert(prio, sub) + self.delta2_enable_blossom(sub) # Delete the expanded blossom. self.nontrivial_blossom.remove(blossom) @@ -1546,25 +1622,9 @@ class _MatchingContext: continue if by.label == _LABEL_S: - # Update tracking of least-slack edges between S-blossoms. - # Priority is edge slack plus 2 times the running sum of - # delta steps. - if self.delta3_node[e] is None: - prio_2x = self.edge_pseudo_slack_2x(e) - if self.graph.integer_weights: - # If all edge weights are integers, the slack of - # any edge between S-vertices is also an integer. - assert prio_2x % 2 == 0 - prio = prio_2x // 2 - else: - prio = prio_2x / 2 - self.delta3_node[e] = self.delta3_queue.insert(prio, e) + self.delta3_add_edge(e) else: - # Update tracking of least-slack edges from vertex "y" to - # any S-vertex. We do this for T-vertices and unlabeled - # vertices. Edges which already have zero slack are still - # tracked. - self.lset_add_vertex_edge(y, by, e) + self.delta2_add_edge(e, y, by) self.scan_queue.clear() @@ -1604,7 +1664,7 @@ class _MatchingContext: # Compute delta2: minimum slack of any edge between an S-vertex and # an unlabeled vertex. # This takes time O(log(n)). - (e, slack) = self.lset_get_best_vertex_edge() + (e, slack) = self.delta2_get_min_edge() if (e != -1) and (slack <= delta_2x): delta_type = 2 delta_2x = slack @@ -1612,31 +1672,12 @@ class _MatchingContext: # Compute delta3: half minimum slack of any edge between two top-level # S-blossoms. - # - # This loop iterates O(m) times per stage. - # Each iteration takes time O(log(n)). - while not self.delta3_queue.empty(): - delta3_node = self.delta3_queue.find_min() - e = delta3_node.data - (x, y, _w) = self.graph.edges[e] - 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. - slack = delta3_node.prio - self.delta_sum_2x - if slack <= delta_2x: - delta_type = 3 - delta_2x = slack - delta_edge = e - break - - # Reject edges between vertices within the same top-level blossom. - # Although intra-blossom edges are never inserted into the queue, - # existing edges in the queue may become intra-blossom when - # a new blossom is formed. - self.delta3_queue.delete(delta3_node) - self.delta3_node[e] = None + # This takes total time O(m * log(n)) per stage. + (e, slack) = self.delta3_get_min_edge() + if (e != -1) and (slack <= delta_2x): + delta_type = 3 + delta_2x = slack + delta_edge = e # Compute delta4: half minimum dual variable of a top-level T-blossom. # This takes time O(log(n)).