1
0
Fork 0

Simplify naming related to double weights

This commit is contained in:
Joris van Rantwijk 2023-02-06 15:39:27 +01:00
parent d4bfb712d2
commit 0f7423e2b8
1 changed files with 82 additions and 88 deletions

View File

@ -6,7 +6,7 @@ from __future__ import annotations
import sys import sys
import math import math
from typing import cast, NamedTuple, Optional from typing import NamedTuple, Optional
def maximum_weight_matching( def maximum_weight_matching(
@ -86,16 +86,7 @@ def maximum_weight_matching(
# Verification is a redundant step; if the matching algorithm is correct, # Verification is a redundant step; if the matching algorithm is correct,
# verification will always pass. # verification will always pass.
if graph.integer_weights: if graph.integer_weights:
# TODO : Maybe interesting to redesign blossom/dual data structures such _verify_optimum(matching)
# that this info for verification is easier to extract.
blossom_dual_var = [
(2 * blossom.half_dual_var if blossom is not None else 0)
for blossom in matching.blossom]
_verify_optimum(graph,
pairs,
cast(list[int], matching.dual_var),
matching.blossom_parent,
cast(list[int], blossom_dual_var))
return pairs return pairs
@ -428,9 +419,9 @@ class _Blossom:
# Every blossom has a variable in the dual LPP. # Every blossom has a variable in the dual LPP.
# #
# "half_dual_var" is half of the current value of the dual variable. # "dual_var" is the current value of the dual variable.
# New blossoms start with dual variable 0. # New blossoms start with dual variable 0.
self.half_dual_var: int|float = 0 self.dual_var: int|float = 0
class _PartialMatching: class _PartialMatching:
@ -497,26 +488,26 @@ class _PartialMatching:
# Every vertex has a variable in the dual LPP. # Every vertex has a variable in the dual LPP.
# #
# "dual_var[v]" is the current value of the dual variable of "v". # "dual_var_2x[v]" is 2 times the dual variable of "v".
# Multiplication by 2 ensures that the values are integers
# if all edge weights are integers.
# #
# Vertex duals are initialized to half the maximum edge weight. # Vertex duals are initialized to half the maximum edge weight.
# Note that we multiply all edge weights by 2, and half of 2 times
# the maximum edge weight is simply the maximum edge weight.
max_weight = max(wt for (_i, _j, wt) in graph.edges) max_weight = max(wt for (_i, _j, wt) in graph.edges)
self.dual_var: list[int|float] = graph.num_vertex * [max_weight] self.dual_var_2x: list[int|float] = graph.num_vertex * [max_weight]
def edge_slack(self, e: int) -> int|float: def edge_slack_2x(self, e: int) -> int|float:
"""Return the slack of the edge with index "e". """Return 2 times the slack of the edge with index "e".
The result is only valid for edges that are not between vertices The result is only valid for edges that are not between vertices
that belong to the same top-level blossom. that belong to the same top-level blossom.
Slack values are integers if all edge weights are even integers. Multiplication by 2 ensures that the return value is an integer
For this reason, we multiply all edge weights by 2. if all edge weights are integers.
""" """
(i, j, wt) = self.graph.edges[e] (i, j, wt) = self.graph.edges[e]
assert self.vertex_blossom[i] != self.vertex_blossom[j] assert self.vertex_blossom[i] != self.vertex_blossom[j]
return self.dual_var[i] + self.dual_var[j] - 2 * wt return self.dual_var_2x[i] + self.dual_var_2x[j] - 2 * wt
def get_blossom(self, b: int) -> _Blossom: def get_blossom(self, b: int) -> _Blossom:
"""Return the Blossom instance for blossom index "b".""" """Return the Blossom instance for blossom index "b"."""
@ -818,7 +809,7 @@ def _make_blossom(
continue continue
# Keep only the least-slack edge to "vblossom". # Keep only the least-slack edge to "vblossom".
slack = matching.edge_slack(e) slack = matching.edge_slack_2x(e)
if ((best_edge_to_blossom[vblossom] == -1) if ((best_edge_to_blossom[vblossom] == -1)
or (slack < best_slack_to_blossom[vblossom])): or (slack < best_slack_to_blossom[vblossom])):
best_edge_to_blossom[vblossom] = e best_edge_to_blossom[vblossom] = e
@ -834,7 +825,7 @@ def _make_blossom(
best_edge = -1 best_edge = -1
best_slack: int|float = 0 best_slack: int|float = 0
for e in best_edge_set: for e in best_edge_set:
slack = matching.edge_slack(e) slack = matching.edge_slack_2x(e)
if (best_edge == -1) or (slack < best_slack): if (best_edge == -1) or (slack < best_slack):
best_edge = e best_edge = e
best_slack = slack best_slack = slack
@ -1007,7 +998,7 @@ def _substage_scan(
# Check whether this edge is tight (has zero slack). # Check whether this edge is tight (has zero slack).
# Only tight edges may be part of an alternating tree. # Only tight edges may be part of an alternating tree.
slack = matching.edge_slack(e) slack = matching.edge_slack_2x(e)
if slack <= 0: if slack <= 0:
if wlabel == _LABEL_NONE: if wlabel == _LABEL_NONE:
# Assign label T to the blossom that contains vertex "w". # Assign label T to the blossom that contains vertex "w".
@ -1024,7 +1015,8 @@ def _substage_scan(
elif wlabel == _LABEL_S: elif wlabel == _LABEL_S:
# Update tracking of least-slack edges between S-blossoms. # Update tracking of least-slack edges between S-blossoms.
best_edge = stage_data.blossom_best_edge[vblossom] best_edge = stage_data.blossom_best_edge[vblossom]
if best_edge < 0 or slack < matching.edge_slack(best_edge): if ((best_edge < 0)
or (slack < matching.edge_slack_2x(best_edge))):
stage_data.blossom_best_edge[vblossom] = e stage_data.blossom_best_edge[vblossom] = e
# Update the list of least-slack edges to S-blossoms for # Update the list of least-slack edges to S-blossoms for
@ -1043,7 +1035,7 @@ def _substage_scan(
# later. At that point we will need a zero-slack edge to # later. At that point we will need a zero-slack edge to
# relabel vertex "w". # relabel vertex "w".
best_edge = stage_data.vertex_best_edge[w] best_edge = stage_data.vertex_best_edge[w]
if best_edge < 0 or slack < matching.edge_slack(best_edge): if best_edge < 0 or slack < matching.edge_slack_2x(best_edge):
stage_data.vertex_best_edge[w] = e stage_data.vertex_best_edge[w] = e
# No further S vertices to scan, and no augmenting path found. # No further S vertices to scan, and no augmenting path found.
@ -1192,7 +1184,7 @@ def _expand_zero_dual_blossoms(matching: _PartialMatching) -> None:
# the most recent delta step. Those blossoms have dual variable # the most recent delta step. Those blossoms have dual variable
# _exactly_ zero. So this comparison is reliable, even in case # _exactly_ zero. So this comparison is reliable, even in case
# of floating point edge weights. # of floating point edge weights.
if blossom.half_dual_var == 0: if blossom.dual_var == 0:
stack.append(b) stack.append(b)
# Use an explicit stack to avoid deep recursion. # Use an explicit stack to avoid deep recursion.
@ -1217,7 +1209,7 @@ def _expand_zero_dual_blossoms(matching: _PartialMatching) -> None:
else: else:
# Non-trivial sub-blossom. # Non-trivial sub-blossom.
# If its dual variable is zero, we expand it recursively. # If its dual variable is zero, we expand it recursively.
if matching.get_blossom(sub).half_dual_var == 0: if matching.get_blossom(sub).dual_var == 0:
stack.append(sub) stack.append(sub)
else: else:
# This sub-blossom will not be expanded. # This sub-blossom will not be expanded.
@ -1347,8 +1339,9 @@ def _calc_dual_delta(
and the type of delta which obtain the minimum, and the edge or blossom and the type of delta which obtain the minimum, and the edge or blossom
that produces the minimum delta, if applicable. that produces the minimum delta, if applicable.
The returned delta value is an integer if all edge weights are even The returned value is 2 times the actual delta value.
integers. Multiplication by 2 ensures that the result is an integer if all edge
weights are integers.
This function assumes that there is at least one S-vertex. This function assumes that there is at least one S-vertex.
This function takes time O(n). This function takes time O(n).
@ -1363,7 +1356,7 @@ def _calc_dual_delta(
# Compute delta1: minimum dual variable of any S-vertex. # Compute delta1: minimum dual variable of any S-vertex.
delta_type = 1 delta_type = 1
delta = min(matching.dual_var[v] delta_2x = min(matching.dual_var_2x[v]
for v in range(num_vertex) for v in range(num_vertex)
if stage_data.blossom_label[matching.vertex_blossom[v]]) if stage_data.blossom_label[matching.vertex_blossom[v]])
@ -1374,10 +1367,10 @@ def _calc_dual_delta(
if stage_data.blossom_label[vb] == _LABEL_NONE: if stage_data.blossom_label[vb] == _LABEL_NONE:
e = stage_data.vertex_best_edge[v] e = stage_data.vertex_best_edge[v]
if e != -1: if e != -1:
slack = matching.edge_slack(e) slack = matching.edge_slack_2x(e)
if slack <= delta: if slack <= delta_2x:
delta_type = 2 delta_type = 2
delta = slack delta_2x = slack
delta_edge = e delta_edge = e
# Compute delta3: half minimum slack of any edge between two top-level # Compute delta3: half minimum slack of any edge between two top-level
@ -1387,7 +1380,7 @@ def _calc_dual_delta(
and matching.blossom_parent[b] == -1): and matching.blossom_parent[b] == -1):
e = stage_data.blossom_best_edge[b] e = stage_data.blossom_best_edge[b]
if e != -1: if e != -1:
slack = matching.edge_slack(e) slack = matching.edge_slack_2x(e)
if matching.graph.integer_weights: if matching.graph.integer_weights:
# If all edge weights are even integers, the slack # If all edge weights are even integers, the slack
# of any edge between two S blossoms is also an even # of any edge between two S blossoms is also an even
@ -1396,28 +1389,28 @@ def _calc_dual_delta(
slack = slack // 2 slack = slack // 2
else: else:
slack = slack / 2 slack = slack / 2
if slack <= delta: if slack <= delta_2x:
delta_type = 3 delta_type = 3
delta = slack delta_2x = slack
delta_edge = e delta_edge = e
# Compute delta4: half minimum dual variable of any T-blossom. # Compute delta4: half minimum dual variable of any T-blossom.
for b in range(num_vertex, 2 * num_vertex): for b in range(num_vertex, 2 * num_vertex):
if (stage_data.blossom_label[b] == _LABEL_T if (stage_data.blossom_label[b] == _LABEL_T
and matching.blossom_parent[b] == -1): and matching.blossom_parent[b] == -1):
slack = matching.get_blossom(b).half_dual_var slack = matching.get_blossom(b).dual_var
if slack < delta: if slack < delta_2x:
delta_type = 4 delta_type = 4
delta = slack delta_2x = slack
delta_blossom = b delta_blossom = b
return (delta_type, delta, delta_edge, delta_blossom) return (delta_type, delta_2x, delta_edge, delta_blossom)
def _apply_delta_step( def _apply_delta_step(
matching: _PartialMatching, matching: _PartialMatching,
stage_data: _StageData, stage_data: _StageData,
delta: int|float delta_2x: int|float
) -> None: ) -> None:
"""Apply a delta step to the dual LPP variables.""" """Apply a delta step to the dual LPP variables."""
@ -1428,10 +1421,10 @@ def _apply_delta_step(
vlabel = stage_data.blossom_label[matching.vertex_blossom[v]] vlabel = stage_data.blossom_label[matching.vertex_blossom[v]]
if vlabel == _LABEL_S: if vlabel == _LABEL_S:
# S-vertex: subtract delta from dual variable. # S-vertex: subtract delta from dual variable.
matching.dual_var[v] -= delta matching.dual_var_2x[v] -= delta_2x
elif vlabel == _LABEL_T: elif vlabel == _LABEL_T:
# T-vertex: add delta to dual variable. # T-vertex: add delta to dual variable.
matching.dual_var[v] += delta matching.dual_var_2x[v] += delta_2x
# Apply delta to dual variables of top-level non-trivial blossoms. # Apply delta to dual variables of top-level non-trivial blossoms.
for b in range(num_vertex, 2 * num_vertex): for b in range(num_vertex, 2 * num_vertex):
@ -1439,10 +1432,10 @@ def _apply_delta_step(
blabel = stage_data.blossom_label[b] blabel = stage_data.blossom_label[b]
if blabel == _LABEL_S: if blabel == _LABEL_S:
# S-blossom: add 2*delta to dual variable. # S-blossom: add 2*delta to dual variable.
matching.get_blossom(b).half_dual_var += delta matching.get_blossom(b).dual_var += delta_2x
elif blabel == _LABEL_T: elif blabel == _LABEL_T:
# T-blossom: subtract 2*delta from dual variable. # T-blossom: subtract 2*delta from dual variable.
matching.get_blossom(b).half_dual_var -= delta matching.get_blossom(b).dual_var -= delta_2x
def _run_stage(matching: _PartialMatching) -> bool: def _run_stage(matching: _PartialMatching) -> bool:
@ -1492,11 +1485,11 @@ def _run_stage(matching: _PartialMatching) -> bool:
break break
# Calculate delta step in the dual LPP problem. # Calculate delta step in the dual LPP problem.
(delta_type, delta, delta_edge, delta_blossom (delta_type, delta_2x, delta_edge, delta_blossom
) = _calc_dual_delta(matching, stage_data) ) = _calc_dual_delta(matching, stage_data)
# Apply the delta step to the dual variables. # Apply the delta step to the dual variables.
_apply_delta_step(matching, stage_data, delta) _apply_delta_step(matching, stage_data, delta_2x)
if delta_type == 2: if delta_type == 2:
# Use the edge from S-vertex to unlabeled vertex that got # Use the edge from S-vertex to unlabeled vertex that got
@ -1539,13 +1532,7 @@ def _run_stage(matching: _PartialMatching) -> bool:
return (augmenting_path is not None) return (augmenting_path is not None)
def _verify_optimum( def _verify_optimum(matching: _PartialMatching) -> None:
graph: _GraphInfo,
pairs: list[tuple[int, int]],
vertex_dual_var: list[int],
blossom_parent: list[int],
blossom_dual_var: list[int]
) -> None:
"""Verify that the optimum solution has been found. """Verify that the optimum solution has been found.
This function takes time O(m * n). This function takes time O(m * n).
@ -1554,53 +1541,60 @@ def _verify_optimum(
AssertionError: If the solution is not optimal. AssertionError: If the solution is not optimal.
""" """
# Find mate of each matched vertex. num_vertex = matching.graph.num_vertex
# Double-check that each vertex is matched to at most one other.
vertex_mate = (graph.num_vertex) * [-1] vertex_mate = matching.vertex_mate
for (i, j) in pairs: vertex_dual_var_2x = matching.dual_var_2x
assert vertex_mate[i] == -1
assert vertex_mate[j] == -1 # Extract dual values of blossoms
vertex_mate[i] = j blossom_dual_var = [
vertex_mate[j] = i (blossom.dual_var if blossom is not None else 0)
for blossom in matching.blossom]
# Double-check that each matching edge actually exists in the graph. # Double-check that each matching edge actually exists in the graph.
nmatched = 0 num_matched_vertex = 0
for (i, j, _wt) in graph.edges: for v in range(num_vertex):
if vertex_mate[v] != -1:
num_matched_vertex += 1
num_matched_edge = 0
for (i, j, _wt) in matching.graph.edges:
if vertex_mate[i] == j: if vertex_mate[i] == j:
nmatched += 1 num_matched_edge += 1
assert len(pairs) == nmatched
assert num_matched_vertex == 2 * num_matched_edge
# Check that all dual variables are non-negative. # Check that all dual variables are non-negative.
assert min(vertex_dual_var) >= 0 assert min(vertex_dual_var_2x) >= 0
assert min(blossom_dual_var) >= 0 assert min(blossom_dual_var) >= 0
# Count the number of vertices in each blossom. # Count the number of vertices in each blossom.
blossom_nvertex = (2 * graph.num_vertex) * [0] blossom_nvertex = (2 * num_vertex) * [0]
for v in range(graph.num_vertex): for v in range(num_vertex):
b = blossom_parent[v] b = matching.blossom_parent[v]
while b != -1: while b != -1:
blossom_nvertex[b] += 1 blossom_nvertex[b] += 1
b = blossom_parent[b] b = matching.blossom_parent[b]
# Calculate slack of each edge. # Calculate slack of each edge.
# Also count the number of matched edges in each blossom. # Also count the number of matched edges in each blossom.
blossom_nmatched = (2 * graph.num_vertex) * [0] blossom_nmatched = (2 * num_vertex) * [0]
for (i, j, wt) in graph.edges: for (i, j, wt) in matching.graph.edges:
# List blossoms that contain vertex "i". # List blossoms that contain vertex "i".
iblossoms = [] iblossoms = []
bi = blossom_parent[i] bi = matching.blossom_parent[i]
while bi != -1: while bi != -1:
iblossoms.append(bi) iblossoms.append(bi)
bi = blossom_parent[bi] bi = matching.blossom_parent[bi]
# List blossoms that contain vertex "j". # List blossoms that contain vertex "j".
jblossoms = [] jblossoms = []
bj = blossom_parent[j] bj = matching.blossom_parent[j]
while bj != -1: while bj != -1:
jblossoms.append(bj) jblossoms.append(bj)
bj = blossom_parent[bj] bj = matching.blossom_parent[bj]
# List blossoms that contain the edge (i, j). # List blossoms that contain the edge (i, j).
edge_blossoms = [] edge_blossoms = []
@ -1613,9 +1607,9 @@ def _verify_optimum(
# dual[i] + dual[j] - weight # dual[i] + dual[j] - weight
# + sum(dual[b] for blossoms "b" containing the edge) # + sum(dual[b] for blossoms "b" containing the edge)
# #
# Note we always multiply edge weights by 2. # Multiply weights by 2 to ensure integer values.
slack = vertex_dual_var[i] + vertex_dual_var[j] - 2 * wt slack = vertex_dual_var_2x[i] + vertex_dual_var_2x[j] - 2 * wt
slack += sum(blossom_dual_var[b] for b in edge_blossoms) slack += 2 * sum(blossom_dual_var[b] for b in edge_blossoms)
# Check that all edges have non-negative slack. # Check that all edges have non-negative slack.
assert slack >= 0 assert slack >= 0
@ -1630,14 +1624,14 @@ def _verify_optimum(
blossom_nmatched[b] += 1 blossom_nmatched[b] += 1
# Check that all unmatched vertices have zero dual. # Check that all unmatched vertices have zero dual.
for v in range(graph.num_vertex): for v in range(num_vertex):
if vertex_mate[v] == -1: if vertex_mate[v] == -1:
assert vertex_dual_var[v] == 0 assert vertex_dual_var_2x[v] == 0
# Check that all blossoms with positive dual are "full". # Check that all blossoms with positive dual are "full".
# A blossom is full if all except one of its vertices are matched # A blossom is full if all except one of its vertices are matched
# to another vertex in the same blossom. # to another vertex in the same blossom.
for b in range(graph.num_vertex, 2 * graph.num_vertex): for b in range(num_vertex, 2 * num_vertex):
if blossom_dual_var[b] > 0: if blossom_dual_var[b] > 0:
assert blossom_nvertex[b] == 2 * blossom_nmatched[b] + 1 assert blossom_nvertex[b] == 2 * blossom_nmatched[b] + 1