1
0
Fork 0

Lazy delta updates of S-vertex duals

This commit is contained in:
Joris van Rantwijk 2024-05-25 23:18:15 +02:00
parent a23c38eb70
commit de03796d99
1 changed files with 152 additions and 98 deletions

View File

@ -536,7 +536,15 @@ class _MatchingContext:
self.start_vertex_dual_2x = max(w for (_x, _y, w) in graph.edges)
# Every vertex has a variable in the dual LPP.
#
# For an unlabeled vertex "x",
# "vertex_dual_2x[x]" is 2 times the dual variable of vertex "x".
#
# For an S-vertex "x",
# "vertex_dual_2x[x]" is 2 times the dual variable of vertex "x"
# plus two times the running sum of delta updates.
#
# For a T-vertex "x", ... TODO
self.vertex_dual_2x: list[float]
self.vertex_dual_2x = num_vertex * [self.start_vertex_dual_2x]
@ -560,39 +568,33 @@ class _MatchingContext:
self.vertex_best_edge: list[int] = num_vertex * [-1]
# Queue of S-vertices to be scanned.
self.queue: collections.deque[int] = collections.deque()
self.scan_queue: collections.deque[int] = collections.deque()
def edge_slack_2x(self, e: int) -> float:
"""Return 2 times the slack of the edge with index "e".
def edge_slack_2x(
self,
x: int,
y: int,
bx: _Blossom,
by: _Blossom,
w: float
) -> float:
"""Calculate edge slack.
The result is only valid for edges that are not between vertices
that belong to the same top-level blossom.
Return 2 times the slack of an edge from vertex "x" in blossom "bx"
to vertex "y" in blossom "by" with weight "w".
The two vertices must be in different top-level blossoms.
Multiplication by 2 ensures that the return value is an integer
if all edge weights are integers.
This function is called O(n**2) times per stage.
"""
(x, y, w) = self.graph.edges[e]
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]
assert bx is not by
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
if bx.label == _LABEL_S:
dual_2x -= self.delta_sum_2x
if by.label == _LABEL_S:
dual_2x -= self.delta_sum_2x
return dual_2x - 2 * w
#
# Least-slack edge tracking:
@ -635,7 +637,10 @@ class _MatchingContext:
if best_edge == -1:
self.vertex_best_edge[y] = e
else:
best_slack = self.edge_slack_2x(best_edge)
(xx, yy, w) = self.graph.edges[best_edge]
bx = self.vertex_top_blossom[xx]
by = self.vertex_top_blossom[yy]
best_slack = self.edge_slack_2x(xx, yy, bx, by, w)
if slack < best_slack:
self.vertex_best_edge[y] = e
@ -657,7 +662,10 @@ class _MatchingContext:
if self.vertex_top_blossom[x].label == _LABEL_NONE:
e = self.vertex_best_edge[x]
if e != -1:
slack = self.edge_slack_2x(e)
(x, y, w) = self.graph.edges[e]
bx = self.vertex_top_blossom[x]
by = self.vertex_top_blossom[y]
slack = self.edge_slack_2x(x, y, bx, by, w)
if (best_index == -1) or (slack < best_slack):
best_index = e
best_slack = slack
@ -668,6 +676,70 @@ class _MatchingContext:
# General support routines:
#
def assign_blossom_label_s(self, blossom: _Blossom) -> None:
"""Assign label S to an unlabeled top-level blossom.
All vertices in the newly labeled blossom are added to the scan queue.
Dual variables are adjusted.
"""
assert blossom.parent is None
assert blossom.label == _LABEL_NONE
# Assign label S.
blossom.label = _LABEL_S
# Adjust the vertex dual variables of the new S-vertices.
vertices = blossom.vertices()
for x in vertices:
self.vertex_dual_2x[x] += self.delta_sum_2x
# Add all vertices inside the newly labeled S-blossom to the queue.
self.scan_queue.extend(vertices)
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
# Remove label.
blossom.label = _LABEL_NONE
# Adjust the vertex dual variables of the former S-vertices.
for x in blossom.vertices():
self.vertex_dual_2x[x] -= self.delta_sum_2x
def assign_blossom_label_t(self, blossom: _Blossom) -> None:
"""Assign label T to an unlabeled top-level blossom."""
assert blossom.parent is None
assert blossom.label == _LABEL_NONE
# Assign label T.
blossom.label = _LABEL_T
# Insert blossom into the delta4 queue.
if isinstance(blossom, _NonTrivialBlossom):
assert blossom.delta4_node is None
prio = blossom.dual_var + self.delta_sum_2x
blossom.delta4_node = self.delta4_queue.insert(prio, blossom)
def remove_blossom_label_t(self, blossom: _Blossom) -> None:
"""Remove label T from a top-level T-blossom."""
assert blossom.parent is None
assert blossom.label == _LABEL_T
# Remove label.
blossom.label = _LABEL_NONE
# Remove blossom from delta4 queue.
if isinstance(blossom, _NonTrivialBlossom):
assert blossom.delta4_node is not None
self.delta4_queue.delete(blossom.delta4_node)
blossom.delta4_node = None
def reset_stage(self) -> None:
"""Reset data which are only valid during a stage.
@ -679,11 +751,16 @@ class _MatchingContext:
# Remove blossom labels.
for blossom in self.trivial_blossom + self.nontrivial_blossom:
if blossom.parent is None:
if blossom.label == _LABEL_S:
self.remove_blossom_label_s(blossom)
elif blossom.label == _LABEL_T:
self.remove_blossom_label_t(blossom)
blossom.label = _LABEL_NONE
blossom.tree_edge = None
# Clear the queue.
self.queue.clear()
# Clear the scan queue.
self.scan_queue.clear()
# Reset least-slack edge tracking.
self.lset_reset()
@ -691,10 +768,7 @@ class _MatchingContext:
# Reset delta queues.
self.delta3_queue.clear()
self.delta3_set.clear()
self.delta4_queue.clear()
for ntb in self.nontrivial_blossom:
ntb.delta4_node = None
assert self.delta4_queue.empty()
def trace_alternating_paths(self, x: int, y: int) -> _AlternatingPath:
"""Trace back through the alternating trees from vertices "x" and "y".
@ -797,16 +871,30 @@ class _MatchingContext:
subblossoms = [self.vertex_top_blossom[x] for (x, y) in path.edges]
# Check that the path is cyclic.
# Note the path may not 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_.
subblossoms_next = [self.vertex_top_blossom[y]
for (x, y) in path.edges]
assert subblossoms[0] == subblossoms_next[-1]
assert subblossoms[1:] == subblossoms_next[:-1]
# Blossom must start and end with an S-sub-blossom.
assert subblossoms[0].label == _LABEL_S
# Relabel the T-sub-blossoms to S-sub-blossoms.
# This also adds new S-vertices to the scan queue.
for sub in subblossoms:
if sub.label == _LABEL_T:
self.remove_blossom_label_t(sub)
self.assign_blossom_label_s(sub)
# Create the new blossom object.
blossom = _NonTrivialBlossom(subblossoms, path.edges)
# Assign label S to the new blossom and link it to the tree.
blossom.label = _LABEL_S
blossom.tree_edge = subblossoms[0].tree_edge
# Insert into the blossom array.
self.nontrivial_blossom.append(blossom)
@ -818,24 +906,6 @@ class _MatchingContext:
for x in blossom.vertices():
self.vertex_top_blossom[x] = blossom
# Assign label S to the new blossom.
assert subblossoms[0].label == _LABEL_S
blossom.label = _LABEL_S
blossom.tree_edge = subblossoms[0].tree_edge
# Former T-vertices which are part of this blossom now become
# S-vertices. Add them to the queue.
for sub in subblossoms:
if sub.label == _LABEL_T:
self.queue.extend(sub.vertices())
# Remove sub-blossoms from the delta4 queue.
for sub in subblossoms:
if ((isinstance(sub, _NonTrivialBlossom))
and (sub.delta4_node is not None)):
self.delta4_queue.delete(sub.delta4_node)
sub.delta4_node = None
@staticmethod
def find_path_through_blossom(
blossom: _NonTrivialBlossom,
@ -875,6 +945,7 @@ class _MatchingContext:
# 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
# Convert sub-blossoms into top-level blossoms.
for sub in blossom.subblossoms:
@ -899,15 +970,9 @@ class _MatchingContext:
sub = self.vertex_top_blossom[y]
# Assign label T to that sub-blossom.
sub.label = _LABEL_T
self.assign_blossom_label_t(sub)
sub.tree_edge = blossom.tree_edge
# Insert T sub-blossom into the delta4 queue.
if isinstance(sub, _NonTrivialBlossom):
assert sub.delta4_node is None
prio = sub.dual_var + self.delta_sum_2x
sub.delta4_node = self.delta4_queue.insert(prio, sub)
# Walk through the expanded blossom from "sub" to the base vertex.
# Assign alternating S and T labels to the sub-blossoms and attach
# them to the alternating tree.
@ -924,20 +989,14 @@ class _MatchingContext:
# Assign label S to path_nodes[p+1].
(y, x) = path_edges[p]
self.assign_label_s(x)
self.extend_tree_s(x)
# Assign label T to path_nodes[i+2] and attach it
# to path_nodes[p+1].
sub = path_nodes[p+2]
sub.label = _LABEL_T
self.assign_blossom_label_t(sub)
sub.tree_edge = path_edges[p+1]
# Insert T sub-blossom into the delta4 queue.
if isinstance(sub, _NonTrivialBlossom):
assert sub.delta4_node is None
prio = sub.dual_var + self.delta_sum_2x
sub.delta4_node = self.delta4_queue.insert(prio, sub)
# Delete the expanded blossom.
self.nontrivial_blossom.remove(blossom)
@ -1109,7 +1168,7 @@ class _MatchingContext:
# Labeling and alternating tree expansion:
#
def assign_label_s(self, x: int) -> None:
def extend_tree_s(self, x: int) -> None:
"""Assign label S to the unlabeled blossom that contains vertex "x".
If vertex "x" is matched, it is attached to the alternating tree
@ -1125,8 +1184,7 @@ class _MatchingContext:
# Assign label S to the blossom that contains vertex "x".
bx = self.vertex_top_blossom[x]
assert bx.label == _LABEL_NONE
bx.label = _LABEL_S
self.assign_blossom_label_s(bx)
y = self.vertex_mate[x]
if y == -1:
@ -1146,10 +1204,7 @@ class _MatchingContext:
# Attach the blossom that contains "x" to the alternating tree.
bx.tree_edge = (y, x)
# Add all vertices inside the newly labeled S-blossom to the queue.
self.queue.extend(bx.vertices())
def assign_label_t(self, x: int, y: int) -> None:
def extend_tree_t(self, x: int, y: int) -> None:
"""Assign label T to the unlabeled blossom that contains vertex "y".
Attach it to the alternating tree via edge (x, y).
@ -1170,20 +1225,13 @@ class _MatchingContext:
by = self.vertex_top_blossom[y]
# Assign label T to the unlabeled blossom.
assert by.label == _LABEL_NONE
by.label = _LABEL_T
self.assign_blossom_label_t(by)
by.tree_edge = (x, y)
# Insert T sub-blossom into the delta4 queue.
if isinstance(by, _NonTrivialBlossom):
assert by.delta4_node is None
prio = by.dual_var + self.delta_sum_2x
by.delta4_node = self.delta4_queue.insert(prio, by)
# Assign label S to the blossom that is mated to the T-blossom.
z = self.vertex_mate[by.base_vertex]
assert z != -1
self.assign_label_s(z)
self.extend_tree_s(z)
def add_s_to_s_edge(self, x: int, y: int) -> Optional[_AlternatingPath]:
"""Add the edge between S-vertices "x" and "y".
@ -1231,10 +1279,10 @@ class _MatchingContext:
# Process S-vertices waiting to be scanned.
# This loop runs through O(n) iterations per stage.
while self.queue:
while self.scan_queue:
# Take a vertex from the queue.
x = self.queue.popleft()
x = self.scan_queue.popleft()
# Double-check that "x" is an S-vertex.
bx = self.vertex_top_blossom[x]
@ -1243,7 +1291,7 @@ class _MatchingContext:
# Scan the edges that are incident on "x".
# This loop runs through O(m) iterations per stage.
for e in adjacent_edges[x]:
(p, q, _w) = edges[e]
(p, q, w) = edges[e]
y = p if p != x else q
# Consider the edge between vertices "x" and "y".
@ -1262,11 +1310,11 @@ class _MatchingContext:
# Check whether this edge is tight (has zero slack).
# Only tight edges may be part of an alternating tree.
slack = self.edge_slack_2x(e)
slack = self.edge_slack_2x(x, y, bx, by, w)
if slack <= 0:
if ylabel == _LABEL_NONE:
# Assign label T to the blossom that contains "y".
self.assign_label_t(x, y)
self.extend_tree_t(x, y)
elif ylabel == _LABEL_S:
# This edge connects two S-blossoms. Use it to find
# either a new blossom or an augmenting path.
@ -1276,8 +1324,17 @@ class _MatchingContext:
elif ylabel == _LABEL_S:
# Update tracking of least-slack edges between S-blossoms.
# Priority is edge slack plus 2 times the running sum of
# delta updates.
if e not in self.delta3_set:
prio = self.edge_slack(e) + self.delta_sum_2x
if self.graph.integer_weights:
# If all edge weights are integers, the slack of
# any edge between S-vertices is also an integer.
assert slack % 2 == 0
slack_1x = slack // 2
else:
slack_1x = slack / 2
prio = slack_1x + self.delta_sum_2x
self.delta3_set.add(e)
self.delta3_queue.insert(prio, e)
@ -1342,7 +1399,7 @@ class _MatchingContext:
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)
slack = delta3_node.prio - self.delta_sum_2x
if slack <= delta_2x:
delta_type = 3
delta_2x = slack
@ -1378,10 +1435,7 @@ class _MatchingContext:
# Apply delta to dual variables of all vertices.
for x in range(num_vertex):
xlabel = self.vertex_top_blossom[x].label
if xlabel == _LABEL_S:
# S-vertex: subtract delta from dual variable.
self.vertex_dual_2x[x] -= delta_2x
elif xlabel == _LABEL_T:
if xlabel == _LABEL_T:
# T-vertex: add delta to dual variable.
self.vertex_dual_2x[x] += delta_2x
@ -1420,12 +1474,12 @@ class _MatchingContext:
# Assign label S to all unmatched vertices and put them in the queue.
for x in range(num_vertex):
if self.vertex_mate[x] == -1:
self.assign_label_s(x)
self.extend_tree_s(x)
# Stop if all vertices are matched.
# No further improvement is possible in that case.
# This avoids messy calculations of delta steps without any S-vertex.
if not self.queue:
if not self.scan_queue:
return False
# Each pass through the following loop is a "substage".
@ -1457,7 +1511,7 @@ class _MatchingContext:
(x, y, _w) = self.graph.edges[delta_edge]
if self.vertex_top_blossom[x].label != _LABEL_S:
(x, y) = (y, x)
self.assign_label_t(x, y)
self.extend_tree_t(x, y)
elif delta_type == 3:
# Use the S-to-S edge that got unlocked by the delta update.