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