Implement heap-based edge tracking for delta3
This commit is contained in:
		
							parent
							
								
									91d4afb271
								
							
						
					
					
						commit
						13b6b76d47
					
				| 
						 | 
				
			
			@ -10,6 +10,8 @@ import collections
 | 
			
		|||
from collections.abc import Sequence
 | 
			
		||||
from typing import NamedTuple, Optional
 | 
			
		||||
 | 
			
		||||
from datastruct import UnionFindQueue, PriorityQueue
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def maximum_weight_matching(
 | 
			
		||||
        edges: Sequence[tuple[int, int, float]]
 | 
			
		||||
| 
						 | 
				
			
			@ -32,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), where "n" is the number of vertices.
 | 
			
		||||
    This function takes time O(n**3 + n*m*log(n)), where "n" is the number of vertices.
 | 
			
		||||
    This function uses O(n + m) memory, where "m" is the number of edges.
 | 
			
		||||
 | 
			
		||||
    Parameters:
 | 
			
		||||
| 
						 | 
				
			
			@ -380,11 +382,6 @@ class _Blossom:
 | 
			
		|||
        # "tree_edge = None" if the blossom is the root of an alternating tree.
 | 
			
		||||
        self.tree_edge: Optional[tuple[int, int]] = None
 | 
			
		||||
 | 
			
		||||
        # For a top-level S-blossom,
 | 
			
		||||
        # "best_edge" is the edge index of the least-slack edge to
 | 
			
		||||
        # a different S-blossom, or -1 if no such edge has been found.
 | 
			
		||||
        self.best_edge: int = -1
 | 
			
		||||
 | 
			
		||||
        # "marker" is a temporary variable used to discover common
 | 
			
		||||
        # ancestors in the blossom tree. It is normally False, except
 | 
			
		||||
        # when used by "trace_alternating_paths()".
 | 
			
		||||
| 
						 | 
				
			
			@ -450,11 +447,6 @@ class _NonTrivialBlossom(_Blossom):
 | 
			
		|||
        # New blossoms start with dual variable 0.
 | 
			
		||||
        self.dual_var: float = 0
 | 
			
		||||
 | 
			
		||||
        # For a non-trivial, top-level S-blossom,
 | 
			
		||||
        # "best_edge_set" is a list of least-slack edges between this blossom
 | 
			
		||||
        # and other S-blossoms.
 | 
			
		||||
        self.best_edge_set: Optional[list[int]] = None
 | 
			
		||||
 | 
			
		||||
    def vertices(self) -> list[int]:
 | 
			
		||||
        """Return a list of vertex indices contained in the blossom."""
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -530,6 +522,15 @@ class _MatchingContext:
 | 
			
		|||
        # Initially all vertices are trivial top-level blossoms.
 | 
			
		||||
        self.vertex_top_blossom: list[_Blossom] = self.trivial_blossom.copy()
 | 
			
		||||
 | 
			
		||||
        # Running sum of applied delta steps times 2.
 | 
			
		||||
        self.delta_sum_2x: float = 0
 | 
			
		||||
 | 
			
		||||
        # Queue containing edges between S-vertices in different top-level
 | 
			
		||||
        # blossoms. The priority of each edge is equal to its slack
 | 
			
		||||
        # plus two times the running sum of delta updates.
 | 
			
		||||
        self.delta3_queue = PriorityQueue()
 | 
			
		||||
        self.delta3_set: set[int] = set()
 | 
			
		||||
 | 
			
		||||
        # Every vertex has a variable in the dual LPP.
 | 
			
		||||
        #
 | 
			
		||||
        # "vertex_dual_2x[x]" is 2 times the dual variable of vertex "x".
 | 
			
		||||
| 
						 | 
				
			
			@ -563,6 +564,23 @@ class _MatchingContext:
 | 
			
		|||
        assert self.vertex_top_blossom[x] is not self.vertex_top_blossom[y]
 | 
			
		||||
        return self.vertex_dual_2x[x] + self.vertex_dual_2x[y] - 2 * w
 | 
			
		||||
 | 
			
		||||
    def edge_slack(self, e: int) -> float:
 | 
			
		||||
        """Return the slack of the edge with index "e".
 | 
			
		||||
 | 
			
		||||
        This function must only be used for edges between two vertices
 | 
			
		||||
        in different top-level S-blossoms. If all edge weights are
 | 
			
		||||
        integers, the result is also an integer.
 | 
			
		||||
        """
 | 
			
		||||
        (x, y, w) = self.graph.edges[e]
 | 
			
		||||
        assert self.vertex_top_blossom[x] is not self.vertex_top_blossom[y]
 | 
			
		||||
        dual_2x = self.vertex_dual_2x[x] + self.vertex_dual_2x[y]
 | 
			
		||||
        if self.graph.integer_weights:
 | 
			
		||||
            assert dual_2x % 2 == 0
 | 
			
		||||
            dual = dual_2x // 2
 | 
			
		||||
        else:
 | 
			
		||||
            dual = dual_2x / 2
 | 
			
		||||
        return dual - w
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # Least-slack edge tracking:
 | 
			
		||||
    #
 | 
			
		||||
| 
						 | 
				
			
			@ -576,16 +594,6 @@ class _MatchingContext:
 | 
			
		|||
    # 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.
 | 
			
		||||
    #
 | 
			
		||||
    # For each top-level S-blossom, we keep track of the least-slack edge
 | 
			
		||||
    # to any S-vertex not in the same blossom.
 | 
			
		||||
    #
 | 
			
		||||
    # Furthermore, for each top-level S-blossom, we keep a list of least-slack
 | 
			
		||||
    # edges to other top-level S-blossoms. For any pair of top-level
 | 
			
		||||
    # S-blossoms, the least-slack edge between them is contained in the edge
 | 
			
		||||
    # list of at least one of the blossoms. An edge list may contain multiple
 | 
			
		||||
    # edges to the same S-blossom. Such redundant edges are pruned during
 | 
			
		||||
    # blossom merging to limit the number of tracked edges.
 | 
			
		||||
    #
 | 
			
		||||
    # 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
 | 
			
		||||
| 
						 | 
				
			
			@ -604,12 +612,8 @@ class _MatchingContext:
 | 
			
		|||
        for x in range(num_vertex):
 | 
			
		||||
            self.vertex_best_edge[x] = -1
 | 
			
		||||
 | 
			
		||||
        for blossom in self.trivial_blossom:
 | 
			
		||||
            blossom.best_edge = -1
 | 
			
		||||
 | 
			
		||||
        for blossom in self.nontrivial_blossom:
 | 
			
		||||
            blossom.best_edge = -1
 | 
			
		||||
            blossom.best_edge_set = None
 | 
			
		||||
        self.delta3_queue.clear()
 | 
			
		||||
        self.delta3_set.clear()
 | 
			
		||||
 | 
			
		||||
    def lset_add_vertex_edge(self, y: int, e: int, slack: float) -> None:
 | 
			
		||||
        """Add edge "e" from an S-vertex to unlabeled vertex or T-vertex "y".
 | 
			
		||||
| 
						 | 
				
			
			@ -650,155 +654,6 @@ class _MatchingContext:
 | 
			
		|||
 | 
			
		||||
        return (best_index, best_slack)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def lset_new_blossom(blossom: _Blossom) -> None:
 | 
			
		||||
        """Start tracking edges for a new S-blossom."""
 | 
			
		||||
        assert blossom.best_edge == -1
 | 
			
		||||
        if isinstance(blossom, _NonTrivialBlossom):
 | 
			
		||||
            assert blossom.best_edge_set is None
 | 
			
		||||
            blossom.best_edge_set = []
 | 
			
		||||
 | 
			
		||||
    def lset_add_blossom_edge(
 | 
			
		||||
            self,
 | 
			
		||||
            blossom: _Blossom,
 | 
			
		||||
            e: int,
 | 
			
		||||
            slack: float
 | 
			
		||||
            ) -> None:
 | 
			
		||||
        """Add edge "e" between the specified S-blossom and another S-blossom.
 | 
			
		||||
 | 
			
		||||
        This function takes time O(1) per call.
 | 
			
		||||
        This function is called O(m) times per stage.
 | 
			
		||||
        """
 | 
			
		||||
        # Track least-slack edge between this blossom and any other S-blossom.
 | 
			
		||||
        if blossom.best_edge == -1:
 | 
			
		||||
            blossom.best_edge = e
 | 
			
		||||
        else:
 | 
			
		||||
            best_slack = self.edge_slack_2x(blossom.best_edge)
 | 
			
		||||
            if slack < best_slack:
 | 
			
		||||
                blossom.best_edge = e
 | 
			
		||||
 | 
			
		||||
        # Regardless of whether this edge is currently the least-slack edge,
 | 
			
		||||
        # this edge may later become the least-slack edge if other edges
 | 
			
		||||
        # become unavailable when S-blossoms are merged.
 | 
			
		||||
        #
 | 
			
		||||
        # We therefore add the edge to a list of potential future least-slack
 | 
			
		||||
        # edges for this blossom. We do this only for non-trivial blossoms.
 | 
			
		||||
        if isinstance(blossom, _NonTrivialBlossom):
 | 
			
		||||
            assert blossom.best_edge_set is not None
 | 
			
		||||
            blossom.best_edge_set.append(e)
 | 
			
		||||
 | 
			
		||||
    def lset_merge_blossoms(self, blossom: _NonTrivialBlossom) -> None:
 | 
			
		||||
        """Update least-slack edge tracking after merging sub-blossoms
 | 
			
		||||
        into a new S-blossom.
 | 
			
		||||
 | 
			
		||||
        This function takes total time O(n**2) per stage.
 | 
			
		||||
        """
 | 
			
		||||
        num_vertex = self.graph.num_vertex
 | 
			
		||||
 | 
			
		||||
        # Calculate the set of least-slack edges to other S-blossoms.
 | 
			
		||||
        # We basically merge the edge lists from all sub-blossoms, but reject
 | 
			
		||||
        # edges that are internal to this blossom, and trim the set such that
 | 
			
		||||
        # there is at most one edge to each external S-blossom.
 | 
			
		||||
        #
 | 
			
		||||
        # Sub-blossoms that were formerly labeled T can be ignored; their
 | 
			
		||||
        # vertices are in the queue and will discover neighbouring S-blossoms
 | 
			
		||||
        # via the edge scan process.
 | 
			
		||||
        #
 | 
			
		||||
        # Build a temporary array holding the least-slack edge index to
 | 
			
		||||
        # each top-level S-blossom. This array is indexed by the base vertex
 | 
			
		||||
        # of the blossoms.
 | 
			
		||||
        best_edge_to_blossom: list[int] = num_vertex * [-1]
 | 
			
		||||
        zero_slack: float = 0
 | 
			
		||||
        best_slack_to_blossom: list[float] = num_vertex * [zero_slack]
 | 
			
		||||
 | 
			
		||||
        # And find the overall least-slack edge to any other S-blossom.
 | 
			
		||||
        best_edge = -1
 | 
			
		||||
        best_slack: float = 0
 | 
			
		||||
 | 
			
		||||
        # Add the least-slack edges of every S-sub-blossom.
 | 
			
		||||
        for sub in blossom.subblossoms:
 | 
			
		||||
 | 
			
		||||
            if sub.label != _LABEL_S:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            if isinstance(sub, _NonTrivialBlossom):
 | 
			
		||||
                # Pull the edge list from the sub-blossom.
 | 
			
		||||
                assert sub.best_edge_set is not None
 | 
			
		||||
                sub_edge_set = sub.best_edge_set
 | 
			
		||||
                # Delete the edge list from the sub-blossom.
 | 
			
		||||
                sub.best_edge_set = None
 | 
			
		||||
            else:
 | 
			
		||||
                # Trivial blossoms don't have a list of least-slack edges,
 | 
			
		||||
                # so we just look at all adjacent edges. This happens at most
 | 
			
		||||
                # once per vertex per stage.
 | 
			
		||||
                # It adds up to O(m) time per stage.
 | 
			
		||||
                sub_edge_set = self.graph.adjacent_edges[sub.base_vertex]
 | 
			
		||||
 | 
			
		||||
            # Add edges to the temporary array.
 | 
			
		||||
            for e in sub_edge_set:
 | 
			
		||||
                (x, y, _w) = self.graph.edges[e]
 | 
			
		||||
                bx = self.vertex_top_blossom[x]
 | 
			
		||||
                by = self.vertex_top_blossom[y]
 | 
			
		||||
                assert (bx is blossom) or (by is blossom)
 | 
			
		||||
 | 
			
		||||
                # Reject internal edges in this blossom.
 | 
			
		||||
                if bx is by:
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                # Set bx = blossom at the other end of this edge.
 | 
			
		||||
                bx = by if (bx is blossom) else bx
 | 
			
		||||
 | 
			
		||||
                # Reject edges that don't link to an S-blossom.
 | 
			
		||||
                if bx.label != _LABEL_S:
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                # Keep only the least-slack edge to "bx".
 | 
			
		||||
                slack = self.edge_slack_2x(e)
 | 
			
		||||
                bx_base = bx.base_vertex
 | 
			
		||||
                if ((best_edge_to_blossom[bx_base] == -1)
 | 
			
		||||
                        or (slack < best_slack_to_blossom[bx_base])):
 | 
			
		||||
                    best_edge_to_blossom[bx_base] = e
 | 
			
		||||
                    best_slack_to_blossom[bx_base] = slack
 | 
			
		||||
 | 
			
		||||
                # Update the overall least-slack edge to any S-blossom.
 | 
			
		||||
                if (best_edge == -1) or (slack < best_slack):
 | 
			
		||||
                    best_edge = e
 | 
			
		||||
                    best_slack = slack
 | 
			
		||||
 | 
			
		||||
        # Extract a compact list of least-slack edge indices.
 | 
			
		||||
        # We can not keep the temporary array because that would blow up
 | 
			
		||||
        # memory use to O(n**2).
 | 
			
		||||
        best_edge_set = [e for e in best_edge_to_blossom if e != -1]
 | 
			
		||||
        blossom.best_edge_set = best_edge_set
 | 
			
		||||
 | 
			
		||||
        # Keep the overall least-slack edge.
 | 
			
		||||
        blossom.best_edge = best_edge
 | 
			
		||||
 | 
			
		||||
    def lset_get_best_blossom_edge(self) -> tuple[int, float]:
 | 
			
		||||
        """Return the index and slack of the least-slack edge between
 | 
			
		||||
        any pair of top-level S-blossoms.
 | 
			
		||||
 | 
			
		||||
        This function takes time O(n) per call.
 | 
			
		||||
        This function takes total time O(n**2) per stage.
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            Tuple (edge_index, slack_2x) if there is a least-slack edge,
 | 
			
		||||
            or (-1, 0) if there is no suitable edge.
 | 
			
		||||
        """
 | 
			
		||||
        best_index = -1
 | 
			
		||||
        best_slack: float = 0
 | 
			
		||||
 | 
			
		||||
        for blossom in self.trivial_blossom + self.nontrivial_blossom:
 | 
			
		||||
            if (blossom.label == _LABEL_S) and (blossom.parent is None):
 | 
			
		||||
                e = blossom.best_edge
 | 
			
		||||
                if e != -1:
 | 
			
		||||
                    slack = self.edge_slack_2x(e)
 | 
			
		||||
                    if (best_index == -1) or (slack < best_slack):
 | 
			
		||||
                        best_index = e
 | 
			
		||||
                        best_slack = slack
 | 
			
		||||
 | 
			
		||||
        return (best_index, best_slack)
 | 
			
		||||
 | 
			
		||||
    #
 | 
			
		||||
    # General support routines:
 | 
			
		||||
    #
 | 
			
		||||
| 
						 | 
				
			
			@ -954,9 +809,6 @@ class _MatchingContext:
 | 
			
		|||
            if sub.label == _LABEL_T:
 | 
			
		||||
                self.queue.extend(sub.vertices())
 | 
			
		||||
 | 
			
		||||
        # Merge least-slack edges for the S-sub-blossoms.
 | 
			
		||||
        self.lset_merge_blossoms(blossom)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def find_path_through_blossom(
 | 
			
		||||
            blossom: _NonTrivialBlossom,
 | 
			
		||||
| 
						 | 
				
			
			@ -1251,9 +1103,6 @@ class _MatchingContext:
 | 
			
		|||
            # Attach the blossom that contains "x" to the alternating tree.
 | 
			
		||||
            bx.tree_edge = (y, x)
 | 
			
		||||
 | 
			
		||||
        # Start least-slack edge tracking for the S-blossom.
 | 
			
		||||
        self.lset_new_blossom(bx)
 | 
			
		||||
 | 
			
		||||
        # Add all vertices inside the newly labeled S-blossom to the queue.
 | 
			
		||||
        self.queue.extend(bx.vertices())
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1378,7 +1227,10 @@ class _MatchingContext:
 | 
			
		|||
 | 
			
		||||
                elif ylabel == _LABEL_S:
 | 
			
		||||
                    # Update tracking of least-slack edges between S-blossoms.
 | 
			
		||||
                    self.lset_add_blossom_edge(bx, e, slack)
 | 
			
		||||
                    if e not in self.delta3_set:
 | 
			
		||||
                        prio = self.edge_slack(e) + self.delta_sum_2x
 | 
			
		||||
                        self.delta3_set.add(e)
 | 
			
		||||
                        self.delta3_queue.insert(prio, e)
 | 
			
		||||
 | 
			
		||||
                if ylabel != _LABEL_S:
 | 
			
		||||
                    # Update tracking of least-slack edges from vertex "y" to
 | 
			
		||||
| 
						 | 
				
			
			@ -1435,20 +1287,28 @@ class _MatchingContext:
 | 
			
		|||
 | 
			
		||||
        # Compute delta3: half minimum slack of any edge between two top-level
 | 
			
		||||
        # S-blossoms.
 | 
			
		||||
        (e, slack) = self.lset_get_best_blossom_edge()
 | 
			
		||||
        if e != -1:
 | 
			
		||||
            if self.graph.integer_weights:
 | 
			
		||||
                # If all edge weights are even integers, the slack
 | 
			
		||||
                # of any edge between two S blossoms is also an even
 | 
			
		||||
                # integer. Therefore the delta is an integer.
 | 
			
		||||
                assert slack % 2 == 0
 | 
			
		||||
                slack = slack // 2
 | 
			
		||||
            else:
 | 
			
		||||
                slack = slack / 2
 | 
			
		||||
            if slack <= delta_2x:
 | 
			
		||||
                delta_type = 3
 | 
			
		||||
                delta_2x = slack
 | 
			
		||||
                delta_edge = e
 | 
			
		||||
        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_top_blossom[x]
 | 
			
		||||
            by = self.vertex_top_blossom[y]
 | 
			
		||||
            assert (bx.label == _LABEL_S) and (by.label == _LABEL_S)
 | 
			
		||||
            if bx is not by:
 | 
			
		||||
                # Found edge between different top-level S-blossoms.
 | 
			
		||||
                slack = self.edge_slack(e)
 | 
			
		||||
                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_set.remove(e)
 | 
			
		||||
 | 
			
		||||
        # Compute delta4: half minimum dual variable of a top-level T-blossom.
 | 
			
		||||
        for blossom in self.nontrivial_blossom:
 | 
			
		||||
| 
						 | 
				
			
			@ -1465,6 +1325,8 @@ class _MatchingContext:
 | 
			
		|||
 | 
			
		||||
        num_vertex = self.graph.num_vertex
 | 
			
		||||
 | 
			
		||||
        self.delta_sum_2x += delta_2x
 | 
			
		||||
 | 
			
		||||
        # Apply delta to dual variables of all vertices.
 | 
			
		||||
        for x in range(num_vertex):
 | 
			
		||||
            xlabel = self.vertex_top_blossom[x].label
 | 
			
		||||
| 
						 | 
				
			
			@ -1498,7 +1360,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).
 | 
			
		||||
        This function takes time O(n**2 + m * log(n)).
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            True if the matching was successfully augmented.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue