diff --git a/python/mwmatching.py b/python/mwmatching.py index 31c527b..254845e 100644 --- a/python/mwmatching.py +++ b/python/mwmatching.py @@ -386,7 +386,7 @@ class _Blossom: self.tree_edge: Optional[tuple[int, int]] = None # For a labeled top-level blossom, - # "alternating_tree" is the set of all top-level blossoms that belong + # "tree_blossoms" is the set of all top-level blossoms that belong # to the same alternating tree. The same set instance is shared by # all top-level blossoms in the tree. self.tree_blossoms: "Optional[set[_Blossom]]" = None @@ -856,77 +856,155 @@ class _MatchingContext: return (-1, math.inf) # - # General support routines: + # Managing blossom labels: # - # TODO -- Although this code is correct, there is a conceptual problem - # that the division of responsibilities is asymmetric. - # For example, assign_blossom_label_s is not the exact - # opposite of remove_blossom_label_s, and remove_blossom_label_s - # has different responsibilities from remove_blossom_label_t. - # This must be fixed by shifting responsibilities or renaming - # functions, otherwise this stuff is impossible to understand. - def assign_blossom_label_s(self, blossom: _Blossom) -> None: - """Assign label S to an unlabeled top-level blossom.""" + """Change an unlabeled top-level blossom into an S-blossom.""" + assert blossom.parent is None assert blossom.label == _LABEL_NONE blossom.label = _LABEL_S + # Labeled blossoms must not be in the delta2 queue. self.delta2_disable_blossom(blossom) - # Prepare for lazy updating of S-blossom dual variable. + # Adjust for lazy updating of S-blossom dual variables. + # + # The true dual value of an unlabeled top-level blossom is + # blossom.dual_var + # + # while the true dual value of a top-level S-blossom is + # blossom.dual_var + ctx.delta_sum_2x + # + # The value of blossom.dual_var must be adjusted accordingly + # when the blossom changes from unlabeled to S-blossom. + # if isinstance(blossom, _NonTrivialBlossom): blossom.dual_var -= self.delta_sum_2x - def remove_blossom_label_s(self, blossom: _Blossom) -> None: - """Remove label S from a top-level S-blossom.""" - assert blossom.parent is None - assert blossom.label == _LABEL_S - blossom.label = _LABEL_NONE - - # Catch up with lazy updates to S-blossom dual variable. - if isinstance(blossom, _NonTrivialBlossom): - blossom.dual_var += self.delta_sum_2x - - def assign_vertex_label_s(self, blossom: _Blossom) -> None: - """Adjust after assigning label S to previously unlabeled vertices.""" - - # Add the new S-vertices to the scan queue. - vertices = blossom.vertices() - self.scan_queue.extend(vertices) - - # Prepare for lazy updating of S-vertex dual variables. + # Apply pending updates to vertex dual variables and prepare + # for lazy updating of S-vertex dual variables. + # + # For S-blossoms, blossom.vertex_dual_offset is always 0. + # + # Furthermore, the true dual value of an unlabeled vertex is + # (vertex_dual_2x[x] + blossom.vertex_dual_offset) / 2 + # + # while the true dual value of an S-vertex is + # (vertex_dual_2x[x] - delta_sum_2x) / 2 + # + # The value of vertex_dual_2x must be adjusted accordingly + # when vertices change from unlabeled to S-vertex. + # vertex_dual_fixup = self.delta_sum_2x + blossom.vertex_dual_offset blossom.vertex_dual_offset = 0 + vertices = blossom.vertices() for x in vertices: self.vertex_dual_2x[x] += vertex_dual_fixup + + # S-vertices do not keep track of potential delta2 edges. self.delta2_clear_vertex(x) + # Add the new S-vertices to the scan queue. + self.scan_queue.extend(vertices) + def assign_blossom_label_t(self, blossom: _Blossom) -> None: - """Assign label T to an unlabeled top-level blossom.""" + """Change an unlabeled top-level blossom into a T-blossom.""" assert blossom.parent is None assert blossom.label == _LABEL_NONE blossom.label = _LABEL_T + # Labeled blossoms must not be in the delta2 queue. self.delta2_disable_blossom(blossom) if isinstance(blossom, _NonTrivialBlossom): - # Prepare for lazy updating of T-blossom dual variables. + # Adjust for lazy updating of T-blossom dual variables. + # + # The true dual value of an unlabeled top-level blossom is + # blossom.dual_var + # + # while the true dual value of a top-level T-blossom is + # blossom.dual_var - ctx.delta_sum_2x + # + # The value of blossom.dual_var must be adjusted accordingly + # when the blossom changes from unlabeled to S-blossom. + # blossom.dual_var += self.delta_sum_2x - # Insert blossom into the delta4 queue. + # Top-level T-blossoms are tracked in the delta4 queue. assert blossom.delta4_node is None blossom.delta4_node = self.delta4_queue.insert(blossom.dual_var, blossom) # Prepare for lazy updating of T-vertex dual variables. + # + # The true dual value of an unlabeled vertex is + # (vertex_dual_2x[x] + blossom.vertex_dual_offset) / 2 + # + # while the true dual value of a T-vertex is + # (vertex_dual_2x[x] + blossom.vertex_dual_offset + delta_sum_2x) / 2 + # + # The value of blossom.vertex_dual_offset must be adjusted accordingly + # when the blossom changes from unlabeled to T-blossom. + # blossom.vertex_dual_offset -= self.delta_sum_2x + def remove_blossom_label_s(self, blossom: _Blossom) -> None: + """Change a top-level S-blossom into an unlabeled blossom. + + For a blossom with "j" vertices and "k" incident edges, + this function takes time O((j + k) * log(n)). + + This function is called at most once per blossom per stage. + It therefore takes total time O((n + m) * log(n)) per stage. + """ + + assert blossom.parent is None + assert blossom.label == _LABEL_S + blossom.label = _LABEL_NONE + + # Unwind lazy delta updates to the S-blossom dual variable. + if isinstance(blossom, _NonTrivialBlossom): + blossom.dual_var += self.delta_sum_2x + + assert blossom.vertex_dual_offset == 0 + vertex_dual_fixup = -self.delta_sum_2x + + edges = self.graph.edges + adjacent_edges = self.graph.adjacent_edges + + for x in blossom.vertices(): + + # Unwind lazy delta updates to S-vertex dual variables. + self.vertex_dual_2x[x] += vertex_dual_fixup + + # Scan the incident edges of all vertices in the blossom. + for e in adjacent_edges[x]: + (p, q, _w) = edges[e] + y = p if p != x else q + + # If this edge is in the delta3 queue, remove it. + # Only edges between S-vertices are tracked for delta3, + # and vertex "x" is no longer an S-vertex. + self.delta3_remove_edge(e) + + by = self.vertex_set_node[y].find() + if by.label == _LABEL_S: + # Edge "e" connects unlabeled vertex "x" to S-vertex "y". + # It must be tracked for delta2 via vertex "x". + self.delta2_add_edge(e, x, blossom) + else: + # Edge "e" connects former S-vertex "x" to T-vertex + # or unlabeled vertex "y". That implies this edge was + # tracked for delta2 via vertex "y", but it must be + # removed now. + self.delta2_remove_edge(e, y, by) + def remove_blossom_label_t(self, blossom: _Blossom) -> None: - """Remove label T from a top-level T-blossom.""" + """Change a top-level T-blossom into an unlabeled blossom.""" assert blossom.parent is None assert blossom.label == _LABEL_T @@ -934,47 +1012,34 @@ class _MatchingContext: if isinstance(blossom, _NonTrivialBlossom): - # Remove blossom from delta4 queue. + # Unlabeled blossoms are not tracked in the delta4 queue. assert blossom.delta4_node is not None self.delta4_queue.delete(blossom.delta4_node) blossom.delta4_node = None - # Unwind lazy updates to T-blossom dual variable. + # Unwind lazy updates to the T-blossom dual variable. blossom.dual_var -= self.delta_sum_2x # Unwind lazy updates of T-vertex dual variables. blossom.vertex_dual_offset += self.delta_sum_2x - def remove_vertex_label_s(self, x: int, bx: _Blossom) -> None: - """Adjust delta tracking for S-vertex losings its label. + # Enable unlabeled top-level blossom for delta2 tracking. + self.delta2_enable_blossom(blossom) - This function is called when vertex "x" was an S-vertex but - has just lost its label. This requires adjustments in the tracking - of delta2 and delta3. + def change_s_blossom_to_subblossom(self, blossom: _Blossom) -> None: + """Change a top-level S-blossom into an S-subblossom.""" - This function is takes time O(q * log(n)), - where q is the number of incident edges of vertex "x". - This function is called at most once per vertex per stage. - This function therefore takes ammortized time O(m * log(n)) per stage. - """ + assert blossom.parent is None + assert blossom.label == _LABEL_S + blossom.label = _LABEL_NONE - # Scan the edges that are incident on "x". - edges = self.graph.edges - for e in self.graph.adjacent_edges[x]: - (p, q, _w) = edges[e] - y = p if p != x else q + # Unwind lazy delta updates to the S-blossom dual variable. + if isinstance(blossom, _NonTrivialBlossom): + blossom.dual_var += self.delta_sum_2x - 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.delta2_add_edge(e, x, bx) - - else: - self.delta2_remove_edge(e, y, by) + # + # General support routines: + # def reset_blossom_label(self, blossom: _Blossom) -> None: """Remove blossom label.""" @@ -983,27 +1048,9 @@ class _MatchingContext: assert blossom.label != _LABEL_NONE if blossom.label == _LABEL_S: - - # Remove label. - blossom.label = _LABEL_NONE - - # Unwind lazy delta updates to S-blossom dual variable. - if isinstance(blossom, _NonTrivialBlossom): - blossom.dual_var += self.delta_sum_2x - - # Unwind lazy delta updates to S-vertex dual variables. - assert blossom.vertex_dual_offset == 0 - vertex_dual_fixup = -self.delta_sum_2x - for x in blossom.vertices(): - self.vertex_dual_2x[x] += vertex_dual_fixup - - # Adjust delta tracking for S-vertex losing its label. - self.remove_vertex_label_s(x, blossom) - + self.remove_blossom_label_s(blossom) elif blossom.label == _LABEL_T: - self.remove_blossom_label_t(blossom) - self.delta2_enable_blossom(blossom) def _check_alternating_tree_consistency(self) -> None: """TODO -- remove this function, only for debugging""" @@ -1155,25 +1202,28 @@ class _MatchingContext: # Remove blossom labels. # Mark vertices inside former T-blossoms as S-vertices. for sub in subblossoms: - if sub.label == _LABEL_S: - self.remove_blossom_label_s(sub) - elif sub.label == _LABEL_T: + if sub.label == _LABEL_T: self.remove_blossom_label_t(sub) - self.assign_vertex_label_s(sub) + self.assign_blossom_label_s(sub) + self.change_s_blossom_to_subblossom(sub) # Create the new blossom object. blossom = _NonTrivialBlossom(subblossoms, path.edges) - # Assign label S to the new blossom and link it to the tree. - self.assign_blossom_label_s(blossom) + # Assign label S to the new blossom. + blossom.label = _LABEL_S + # Prepare for lazy updating of S-blossom dual variable. + blossom.dual_var = -self.delta_sum_2x + + # Link the new blossom to the alternating tree. tree_blossoms = subblossoms[0].tree_blossoms assert tree_blossoms is not None blossom.tree_edge = subblossoms[0].tree_edge blossom.tree_blossoms = tree_blossoms tree_blossoms.add(blossom) - # Insert into the blossom array. + # Add to the list of blossoms. self.nontrivial_blossom.add(blossom) # Link the subblossoms to the their new parent. @@ -1215,6 +1265,38 @@ class _MatchingContext: return (nodes, edges) + def expand_unlabeled_blossom(self, blossom: _NonTrivialBlossom) -> None: + """Expand the specified unlabeled blossom. + + This function takes total time O(n*log(n)) per stage. + """ + + assert blossom.parent is None + assert blossom.label == _LABEL_NONE + + # Remove blossom from the delta2 queue. + self.delta2_disable_blossom(blossom) + + # Split union-find structure. + blossom.vertex_set.split() + + # Prepare to push lazy delta updates down to the sub-blossoms. + vertex_dual_offset = blossom.vertex_dual_offset + blossom.vertex_dual_offset = 0 + + # Convert sub-blossoms into top-level blossoms. + for sub in blossom.subblossoms: + assert sub.label == _LABEL_NONE + sub.parent = None + + assert sub.vertex_dual_offset == 0 + sub.vertex_dual_offset = vertex_dual_offset + + self.delta2_enable_blossom(sub) + + # Delete the expanded blossom. + self.nontrivial_blossom.remove(blossom) + def expand_t_blossom(self, blossom: _NonTrivialBlossom) -> None: """Expand the specified T-blossom. @@ -1225,34 +1307,18 @@ class _MatchingContext: assert blossom.label == _LABEL_T assert blossom.delta2_node is None - # Remove expanded blossom from the delta4 queue. - assert blossom.delta4_node is not None - self.delta4_queue.delete(blossom.delta4_node) - blossom.delta4_node = None - # Remove blossom from its alternating tree. tree_blossoms = blossom.tree_blossoms assert tree_blossoms is not None tree_blossoms.remove(blossom) - # Split union-find structure. - blossom.vertex_set.split() + # Remove label T. + self.remove_blossom_label_t(blossom) - # Prepare to push lazy delta updates down to the sub-blossoms. - vertex_dual_fixup = self.delta_sum_2x + blossom.vertex_dual_offset - blossom.vertex_dual_offset = 0 + # Expand the now-unlabeled blossom. + self.expand_unlabeled_blossom(blossom) - # Convert sub-blossoms into top-level blossoms. - for sub in blossom.subblossoms: - assert sub.label == _LABEL_NONE - sub.parent = None - - assert sub.vertex_dual_offset == 0 - sub.vertex_dual_offset = vertex_dual_fixup - - self.delta2_enable_blossom(sub) - - # The expanding blossom was part of an alternating tree, linked to + # The expanded blossom was part of an alternating tree, linked to # a parent node in the tree via one of its subblossoms, and linked to # a child node of the tree via the base vertex. # We must reconstruct this part of the alternating tree, which will @@ -1296,40 +1362,6 @@ class _MatchingContext: sub.tree_blossoms = tree_blossoms tree_blossoms.add(sub) - # Delete the expanded blossom. - self.nontrivial_blossom.remove(blossom) - - def expand_unlabeled_blossom(self, blossom: _NonTrivialBlossom) -> None: - """Expand the specified unlabeled blossom. - - This function takes total time O(n*log(n)) per stage. - """ - - assert blossom.parent is None - assert blossom.label == _LABEL_NONE - - self.delta2_disable_blossom(blossom) - - # Split union-find structure. - blossom.vertex_set.split() - - # Prepare to push lazy delta updates down to the sub-blossoms. - vertex_dual_offset = blossom.vertex_dual_offset - blossom.vertex_dual_offset = 0 - - # Convert sub-blossoms into top-level blossoms. - for sub in blossom.subblossoms: - assert sub.label == _LABEL_NONE - sub.parent = None - - assert sub.vertex_dual_offset == 0 - sub.vertex_dual_offset = vertex_dual_offset - - self.delta2_enable_blossom(sub) - - # Delete the expanded blossom. - self.nontrivial_blossom.remove(blossom) - # # Augmenting: # @@ -1492,7 +1524,6 @@ class _MatchingContext: # Assign label S to the blossom that contains vertex "x". bx = self.vertex_set_node[x].find() self.assign_blossom_label_s(bx) - self.assign_vertex_label_s(bx) y = self.vertex_mate[x] if y == -1: @@ -1712,7 +1743,6 @@ class _MatchingContext: # Assign label S. self.assign_blossom_label_s(bx) - self.assign_vertex_label_s(bx) # Mark blossom as the root of an alternating tree. bx.tree_edge = None