1
0
Fork 0

Clean up magagement of blossom labels

This commit is contained in:
Joris van Rantwijk 2024-06-30 09:11:58 +02:00
parent f8c6b99842
commit b960a85b6c
1 changed files with 175 additions and 145 deletions

View File

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