diff --git a/python/mwmatching.py b/python/mwmatching.py index 37caf24..8f2d62d 100644 --- a/python/mwmatching.py +++ b/python/mwmatching.py @@ -616,13 +616,14 @@ class _MatchingContext: 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 ignores pending changes to vertex duals. + The pseudo-slack of an edge is related to its true slack, but + distorted in a way that makes it invariant under delta steps. - For an edge between two S-vertices (in different top-level blossoms), + 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 of delta steps. - For an edge between an S-vertex and unlabeled vertex, + If the edge connects an S-vertex to an unlabeled vertex, the true slack is the pseudo-slack minus the running sum of delta steps, plus the pending offset of the top-level blossom that contains the unlabeled vertex. @@ -630,38 +631,6 @@ class _MatchingContext: (x, y, w) = self.graph.edges[e] return self.vertex_dual_2x[x] + self.vertex_dual_2x[y] - 2 * w - def edge_slack_2x( - self, - x: int, - y: int, - bx: _Blossom, - by: _Blossom, - w: float - ) -> float: - """Calculate edge slack. - - Return 2 times the slack of an edge from vertex "x" in blossom "bx" - to vertex "y" in blossom "by" with weight "w". - - The two vertices must be in different top-level blossoms. - - Multiplication by 2 ensures that the return value is an integer - if all edge weights are integers. - """ - assert bx is not by - dual_2x = self.vertex_dual_2x[x] + self.vertex_dual_2x[y] - if bx.label == _LABEL_S: - dual_2x -= self.delta_sum_2x - if by.label == _LABEL_S: - dual_2x -= self.delta_sum_2x - if bx.label == _LABEL_T: - dual_2x += self.delta_sum_2x - if by.label == _LABEL_T: - dual_2x += self.delta_sum_2x - dual_2x += bx.vertex_dual_offset - dual_2x += by.vertex_dual_offset - return dual_2x - 2 * w - # # Least-slack edge tracking: # @@ -1428,16 +1397,14 @@ class _MatchingContext: else: return path - def substage_scan(self) -> Optional[_AlternatingPath]: - """Scan queued S-vertices to expand the alternating trees. + def substage_scan(self) -> None: + """Scan queued S-vertices and consider their incident edges. - The scan proceeds until either an augmenting path is found, - or the queue of S-vertices becomes empty. - - New blossoms may be created during the scan. - - Returns: - Augmenting path if found; otherwise None. + Edges are inserted in delta2 and delta3 tracking. + This function does not yet use the edges to extend the alternating + tree or find blossoms or augmenting paths, even if the edges + are tight. An edge that is already tight may be used later through + a zero-delta step. """ edges = self.graph.edges @@ -1461,59 +1428,42 @@ class _MatchingContext: y = p if p != x else q # Consider the edge between vertices "x" and "y". + # Update delta2 or delta3 tracking accordingly. + # + # We don't actually use the edge right now to extend + # the alternating tree or create a blossom or alternating path. + # If appropriate, insert this edge into delta2 or delta3 + # tracking. + # Insert this edge into delta2 or delta3 tracking # Try to pull this edge into an alternating tree. - # Note: blossom index of vertex "x" may change during - # this loop, so we need to refresh it here. - bx = self.vertex_set_node[x].find() - by = self.vertex_set_node[y].find() - # Ignore edges that are internal to a blossom. + by = self.vertex_set_node[y].find() if bx is by: continue - ylabel = by.label - - # Check whether this edge is tight (has zero slack). - # Only tight edges may be part of an alternating tree. - slack = self.edge_slack_2x(x, y, bx, by, w) - if slack <= 0: - if ylabel == _LABEL_NONE: - # Assign label T to the blossom that contains "y". - self.extend_tree_t(x, y) - elif ylabel == _LABEL_S: - # This edge connects two S-blossoms. Use it to find - # either a new blossom or an augmenting path. - alternating_path = self.add_s_to_s_edge(x, y) - if alternating_path is not None: - return alternating_path - - elif ylabel == _LABEL_S: + 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 e not in self.delta3_set: + 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 slack % 2 == 0 - slack_1x = slack // 2 + assert prio_2x % 2 == 0 + prio = prio_2x // 2 else: - slack_1x = slack / 2 - prio = slack_1x + self.delta_sum_2x + prio = prio_2x / 2 self.delta3_set.add(e) self.delta3_queue.insert(prio, e) - - if ylabel != _LABEL_S: + 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) - # No further S vertices to scan, and no augmenting path found. - return None - # # Delta steps: # @@ -1640,11 +1590,8 @@ class _MatchingContext: augmenting_path = None while True: - # Expand alternating trees. - # End the stage if an augmenting path is found. - augmenting_path = self.substage_scan() - if augmenting_path is not None: - break + # Consider the incident edges of newly labeled S-vertices. + self.substage_scan() # Calculate delta step in the dual LPP problem. (delta_type, delta_2x, delta_edge, delta_blossom