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.
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.
This function uses O(n + m) memory.
@ -544,15 +544,6 @@ class _MatchingContext:
# Initially there are no non-trivial blossoms.
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
# union-find datastructure of its top-level blossom.
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:
"""Return 2 times the slack of the edge with index "e"."""
(x, y, w) = self.graph.edges[e]
bx = self.vertex_top_blossom[x]
by = self.vertex_top_blossom[y]
bx = self.vertex_set_node[x].find()
by = self.vertex_set_node[y].find()
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
path that starts and ends in an unmatched vertex.
This function takes time O(k) to discover a blossom, where "k" is the
number of sub-blossoms, or time O(n) to discover an augmenting path.
This function takes time O(k*log(n)) to discover a blossom,
where "k" is the number of sub-blossoms,
or time O(n*log(n)) to discover an augmenting path.
Returns:
Alternating path as an ordered list of edges between top-level
@ -956,7 +948,7 @@ class _MatchingContext:
while x != -1 or y != -1:
# Check if we found a common ancestor.
bx = self.vertex_top_blossom[x]
bx = self.vertex_set_node[x].find()
if bx.marker:
first_common = bx
break
@ -984,8 +976,8 @@ class _MatchingContext:
# If we found a common ancestor, trim the paths so they end there.
if first_common is not None:
assert self.vertex_top_blossom[xedges[-1][0]] is first_common
while self.vertex_top_blossom[yedges[-1][0]] is not first_common:
assert self.vertex_set_node[xedges[-1][0]].find() is first_common
while self.vertex_set_node[yedges[-1][0]].find() is not first_common:
yedges.pop()
# Fuse the two paths.
@ -1009,7 +1001,7 @@ class _MatchingContext:
Assign label S to the new blossom.
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.
@ -1017,12 +1009,12 @@ class _MatchingContext:
assert len(path.edges) >= 3
# 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.
# Note the path will not always start and end with the same _vertex_,
# 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]
assert subblossoms[0] == subblossoms_next[-1]
assert subblossoms[1:] == subblossoms_next[:-1]
@ -1053,10 +1045,6 @@ class _MatchingContext:
for sub in subblossoms:
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.
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:
"""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
@ -1116,12 +1104,6 @@ class _MatchingContext:
assert sub.vertex_dual_offset == 0
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.
prio = sub.vertex_set.min_prio()
if prio < math.inf:
@ -1139,7 +1121,7 @@ class _MatchingContext:
# the alternating tree.
assert blossom.tree_edge is not None
(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.
self.assign_blossom_label_t(sub)
@ -1175,7 +1157,7 @@ class _MatchingContext:
def expand_unlabeled_blossom(self, blossom: _NonTrivialBlossom) -> None:
"""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
@ -1196,12 +1178,6 @@ class _MatchingContext:
assert sub.vertex_dual_offset == 0
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.
prio = sub.vertex_set.min_prio()
if prio < math.inf:
@ -1323,7 +1299,7 @@ class _MatchingContext:
# an unmatched vertex or a blossom with unmatched base.
assert len(path.edges) % 2 == 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
# The augmenting path looks like this:
@ -1341,11 +1317,11 @@ class _MatchingContext:
# Augment the non-trivial blossoms on either side of this edge.
# No action is necessary for trivial blossoms.
bx = self.vertex_top_blossom[x]
bx = self.vertex_set_node[x].find()
if isinstance(bx, _NonTrivialBlossom):
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):
self.augment_blossom(by, self.trivial_blossom[y])
@ -1372,7 +1348,7 @@ class _MatchingContext:
"""
# 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_vertex_label_s(bx)
@ -1388,7 +1364,7 @@ class _MatchingContext:
else:
# 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
# Attach the blossom that contains "x" to the alternating tree.
@ -1405,14 +1381,14 @@ class _MatchingContext:
- "y" is an unlabeled, matched vertex.
- 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.
while isinstance(by, _NonTrivialBlossom) and (by.dual_var == 0):
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.
self.assign_blossom_label_t(by)
@ -1446,7 +1422,7 @@ class _MatchingContext:
# but not necessarily in the same vertex within that blossom.
p = path.edges[0][0]
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)
return None
else:
@ -1475,7 +1451,7 @@ class _MatchingContext:
x = self.scan_queue.popleft()
# 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
# Scan the edges that are incident on "x".
@ -1489,8 +1465,8 @@ class _MatchingContext:
# Note: blossom index of vertex "x" may change during
# this loop, so we need to refresh it here.
bx = self.vertex_top_blossom[x]
by = self.vertex_top_blossom[y]
bx = self.vertex_set_node[x].find()
by = self.vertex_set_node[y].find()
# Ignore edges that are internal to a blossom.
if bx is by:
@ -1589,8 +1565,8 @@ class _MatchingContext:
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]
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.
@ -1634,7 +1610,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 + m * log(n)).
This function takes time O((n + m) * log(n)).
Returns:
True if the matching was successfully augmented.
@ -1684,7 +1660,7 @@ class _MatchingContext:
# Use the edge from S-vertex to unlabeled vertex that got
# unlocked through the delta update.
(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)
self.extend_tree_t(x, y)