1
0
Fork 0

Use UnionFind to find top-level blossom of vertex

The run time should now be O(n*m*log(n))
This commit is contained in:
Joris van Rantwijk 2024-05-29 22:01:26 +02:00
parent 225311dae0
commit 9ee26584ab
1 changed files with 31 additions and 55 deletions

View File

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