Restructure Python code as package
This commit is contained in:
parent
f2e8ca1357
commit
147640329f
|
@ -0,0 +1,11 @@
|
||||||
|
"""
|
||||||
|
Algorithm for finding a maximum weight matching in general graphs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .algorithm import (maximum_weight_matching,
|
||||||
|
adjust_weights_for_maximum_cardinality_matching,
|
||||||
|
MatchingError)
|
||||||
|
|
||||||
|
__all__ = ["maximum_weight_matching",
|
||||||
|
"adjust_weights_for_maximum_cardinality_matching",
|
||||||
|
"MatchingError"]
|
|
@ -10,7 +10,7 @@ import math
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from typing import NamedTuple, Optional
|
from typing import NamedTuple, Optional
|
||||||
|
|
||||||
from datastruct import UnionFindQueue, PriorityQueue
|
from .datastruct import UnionFindQueue, PriorityQueue
|
||||||
|
|
||||||
|
|
||||||
def maximum_weight_matching(
|
def maximum_weight_matching(
|
||||||
|
@ -66,10 +66,10 @@ def maximum_weight_matching(
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# Initialize graph representation.
|
# Initialize graph representation.
|
||||||
graph = _GraphInfo(edges)
|
graph = GraphInfo(edges)
|
||||||
|
|
||||||
# Initialize the matching algorithm.
|
# Initialize the matching algorithm.
|
||||||
ctx = _MatchingContext(graph)
|
ctx = MatchingContext(graph)
|
||||||
ctx.start()
|
ctx.start()
|
||||||
|
|
||||||
# Improve the solution until no further improvement is possible.
|
# Improve the solution until no further improvement is possible.
|
||||||
|
@ -92,7 +92,7 @@ def maximum_weight_matching(
|
||||||
# there is a bug in the matching algorithm.
|
# there is a bug in the matching algorithm.
|
||||||
# Verification only works reliably for integer weights.
|
# Verification only works reliably for integer weights.
|
||||||
if graph.integer_weights:
|
if graph.integer_weights:
|
||||||
_verify_optimum(ctx)
|
verify_optimum(ctx)
|
||||||
|
|
||||||
return pairs
|
return pairs
|
||||||
|
|
||||||
|
@ -277,7 +277,7 @@ def _remove_negative_weight_edges(
|
||||||
return edges
|
return edges
|
||||||
|
|
||||||
|
|
||||||
class _GraphInfo:
|
class GraphInfo:
|
||||||
"""Representation of the input graph.
|
"""Representation of the input graph.
|
||||||
|
|
||||||
These data remain unchanged while the algorithm runs.
|
These data remain unchanged while the algorithm runs.
|
||||||
|
@ -328,12 +328,12 @@ class _GraphInfo:
|
||||||
|
|
||||||
|
|
||||||
# Each vertex may be labeled "S" (outer) or "T" (inner) or be unlabeled.
|
# Each vertex may be labeled "S" (outer) or "T" (inner) or be unlabeled.
|
||||||
_LABEL_NONE = 0
|
LABEL_NONE = 0
|
||||||
_LABEL_S = 1
|
LABEL_S = 1
|
||||||
_LABEL_T = 2
|
LABEL_T = 2
|
||||||
|
|
||||||
|
|
||||||
class _Blossom:
|
class Blossom:
|
||||||
"""Represents a blossom in a partially matched graph.
|
"""Represents a blossom in a partially matched graph.
|
||||||
|
|
||||||
A blossom is an odd-length alternating cycle over sub-blossoms.
|
A blossom is an odd-length alternating cycle over sub-blossoms.
|
||||||
|
@ -361,7 +361,7 @@ class _Blossom:
|
||||||
#
|
#
|
||||||
# If this is a top-level blossom,
|
# If this is a top-level blossom,
|
||||||
# "parent = None".
|
# "parent = None".
|
||||||
self.parent: Optional[_NonTrivialBlossom] = None
|
self.parent: Optional[NonTrivialBlossom] = None
|
||||||
|
|
||||||
# "base_vertex" is the vertex index of the base of the blossom.
|
# "base_vertex" is the vertex index of the base of the blossom.
|
||||||
# This is the unique vertex which is contained in the blossom
|
# This is the unique vertex which is contained in the blossom
|
||||||
|
@ -374,7 +374,7 @@ class _Blossom:
|
||||||
# A top-level blossom that is part of an alternating tree,
|
# A top-level blossom that is part of an alternating tree,
|
||||||
# has label S or T. An unlabeled top-level blossom is not part
|
# has label S or T. An unlabeled top-level blossom is not part
|
||||||
# of any alternating tree.
|
# of any alternating tree.
|
||||||
self.label: int = _LABEL_NONE
|
self.label: int = LABEL_NONE
|
||||||
|
|
||||||
# A labeled top-level blossoms keeps track of the edge through which
|
# A labeled top-level blossoms keeps track of the edge through which
|
||||||
# it is attached to the alternating tree.
|
# it is attached to the alternating tree.
|
||||||
|
@ -389,11 +389,11 @@ class _Blossom:
|
||||||
# "tree_blossoms" 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
|
||||||
|
|
||||||
# Each top-level blossom maintains a union-find datastructure
|
# Each top-level blossom maintains a union-find datastructure
|
||||||
# containing all vertices in the blossom.
|
# containing all vertices in the blossom.
|
||||||
self.vertex_set: "UnionFindQueue[_Blossom, int]"
|
self.vertex_set: "UnionFindQueue[Blossom, int]"
|
||||||
self.vertex_set = UnionFindQueue(self)
|
self.vertex_set = UnionFindQueue(self)
|
||||||
|
|
||||||
# If this is a top-level unlabeled blossom with an edge to an
|
# If this is a top-level unlabeled blossom with an edge to an
|
||||||
|
@ -415,7 +415,7 @@ class _Blossom:
|
||||||
return [self.base_vertex]
|
return [self.base_vertex]
|
||||||
|
|
||||||
|
|
||||||
class _NonTrivialBlossom(_Blossom):
|
class NonTrivialBlossom(Blossom):
|
||||||
"""Represents a non-trivial blossom in a partially matched graph.
|
"""Represents a non-trivial blossom in a partially matched graph.
|
||||||
|
|
||||||
A non-trivial blossom is a blossom that contains multiple sub-blossoms
|
A non-trivial blossom is a blossom that contains multiple sub-blossoms
|
||||||
|
@ -436,7 +436,7 @@ class _NonTrivialBlossom(_Blossom):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
subblossoms: list[_Blossom],
|
subblossoms: list[Blossom],
|
||||||
edges: list[tuple[int, int]]
|
edges: list[tuple[int, int]]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a new blossom."""
|
"""Initialize a new blossom."""
|
||||||
|
@ -454,7 +454,7 @@ class _NonTrivialBlossom(_Blossom):
|
||||||
#
|
#
|
||||||
# "subblossoms[0]" is the start and end of the alternating cycle.
|
# "subblossoms[0]" is the start and end of the alternating cycle.
|
||||||
# "subblossoms[0]" contains the base vertex of the blossom.
|
# "subblossoms[0]" contains the base vertex of the blossom.
|
||||||
self.subblossoms: list[_Blossom] = subblossoms
|
self.subblossoms: list[Blossom] = subblossoms
|
||||||
|
|
||||||
# "edges" is a list of edges linking the sub-blossoms.
|
# "edges" is a list of edges linking the sub-blossoms.
|
||||||
# Each edge is represented as an ordered pair "(x, y)" where "x"
|
# Each edge is represented as an ordered pair "(x, y)" where "x"
|
||||||
|
@ -491,13 +491,13 @@ class _NonTrivialBlossom(_Blossom):
|
||||||
"""Return a list of vertex indices contained in the blossom."""
|
"""Return a list of vertex indices contained in the blossom."""
|
||||||
|
|
||||||
# Use an explicit stack to avoid deep recursion.
|
# Use an explicit stack to avoid deep recursion.
|
||||||
stack: list[_NonTrivialBlossom] = [self]
|
stack: list[NonTrivialBlossom] = [self]
|
||||||
nodes: list[int] = []
|
nodes: list[int] = []
|
||||||
|
|
||||||
while stack:
|
while stack:
|
||||||
b = stack.pop()
|
b = stack.pop()
|
||||||
for sub in b.subblossoms:
|
for sub in b.subblossoms:
|
||||||
if isinstance(sub, _NonTrivialBlossom):
|
if isinstance(sub, NonTrivialBlossom):
|
||||||
stack.append(sub)
|
stack.append(sub)
|
||||||
else:
|
else:
|
||||||
nodes.append(sub.base_vertex)
|
nodes.append(sub.base_vertex)
|
||||||
|
@ -505,21 +505,21 @@ class _NonTrivialBlossom(_Blossom):
|
||||||
return nodes
|
return nodes
|
||||||
|
|
||||||
|
|
||||||
class _AlternatingPath(NamedTuple):
|
class AlternatingPath(NamedTuple):
|
||||||
"""Represents a list of edges forming an alternating path or an
|
"""Represents a list of edges forming an alternating path or an
|
||||||
alternating cycle."""
|
alternating cycle."""
|
||||||
edges: list[tuple[int, int]]
|
edges: list[tuple[int, int]]
|
||||||
is_cycle: bool
|
is_cycle: bool
|
||||||
|
|
||||||
|
|
||||||
class _MatchingContext:
|
class MatchingContext:
|
||||||
"""Holds all data used by the matching algorithm.
|
"""Holds all data used by the matching algorithm.
|
||||||
|
|
||||||
It contains a partial solution of the matching problem and several
|
It contains a partial solution of the matching problem and several
|
||||||
auxiliary data structures.
|
auxiliary data structures.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, graph: _GraphInfo) -> None:
|
def __init__(self, graph: GraphInfo) -> None:
|
||||||
"""Set up the initial state of the matching algorithm."""
|
"""Set up the initial state of the matching algorithm."""
|
||||||
|
|
||||||
num_vertex = graph.num_vertex
|
num_vertex = graph.num_vertex
|
||||||
|
@ -545,14 +545,14 @@ class _MatchingContext:
|
||||||
#
|
#
|
||||||
# "trivial_blossom[x]" is the trivial blossom that contains only
|
# "trivial_blossom[x]" is the trivial blossom that contains only
|
||||||
# vertex "x".
|
# vertex "x".
|
||||||
self.trivial_blossom: list[_Blossom] = [_Blossom(x)
|
self.trivial_blossom: list[Blossom] = [Blossom(x)
|
||||||
for x in range(num_vertex)]
|
for x in range(num_vertex)]
|
||||||
|
|
||||||
# Non-trivial blossoms may be created and destroyed during
|
# Non-trivial blossoms may be created and destroyed during
|
||||||
# the course of the algorithm.
|
# the course of the algorithm.
|
||||||
#
|
#
|
||||||
# Initially there are no non-trivial blossoms.
|
# Initially there are no non-trivial blossoms.
|
||||||
self.nontrivial_blossom: set[_NonTrivialBlossom] = set()
|
self.nontrivial_blossom: set[NonTrivialBlossom] = set()
|
||||||
|
|
||||||
# "vertex_set_node[x]" represents the vertex "x" inside the
|
# "vertex_set_node[x]" represents the vertex "x" inside the
|
||||||
# union-find datastructure of its top-level blossom.
|
# union-find datastructure of its top-level blossom.
|
||||||
|
@ -593,7 +593,7 @@ class _MatchingContext:
|
||||||
# Queue containing unlabeled top-level blossoms that have an edge to
|
# Queue containing unlabeled top-level blossoms that have an edge to
|
||||||
# an S-blossom. The priority of a blossom is 2 times its least slack
|
# an S-blossom. The priority of a blossom is 2 times its least slack
|
||||||
# to an S blossom, plus 2 times the running sum of delta steps.
|
# to an S blossom, plus 2 times the running sum of delta steps.
|
||||||
self.delta2_queue: PriorityQueue[_Blossom] = PriorityQueue()
|
self.delta2_queue: PriorityQueue[Blossom] = PriorityQueue()
|
||||||
|
|
||||||
# Queue containing edges between S-vertices in different top-level
|
# Queue containing edges between S-vertices in different top-level
|
||||||
# blossoms. The priority of an edge is its slack plus 2 times the
|
# blossoms. The priority of an edge is its slack plus 2 times the
|
||||||
|
@ -605,7 +605,7 @@ class _MatchingContext:
|
||||||
# Queue containing top-level non-trivial T-blossoms.
|
# Queue containing top-level non-trivial T-blossoms.
|
||||||
# The priority of a blossom is its dual plus 2 times the running
|
# The priority of a blossom is its dual plus 2 times the running
|
||||||
# sum of delta steps.
|
# sum of delta steps.
|
||||||
self.delta4_queue: PriorityQueue[_NonTrivialBlossom] = PriorityQueue()
|
self.delta4_queue: PriorityQueue[NonTrivialBlossom] = PriorityQueue()
|
||||||
|
|
||||||
# For each T-vertex or unlabeled vertex "x",
|
# For each T-vertex or unlabeled vertex "x",
|
||||||
# "vertex_sedge_queue[x]" is a queue of edges between "x" and any
|
# "vertex_sedge_queue[x]" is a queue of edges between "x" and any
|
||||||
|
@ -648,7 +648,7 @@ class _MatchingContext:
|
||||||
(x, y, w) = self.graph.edges[e]
|
(x, y, w) = self.graph.edges[e]
|
||||||
return self.vertex_dual_2x[x] + self.vertex_dual_2x[y] - 2 * w
|
return self.vertex_dual_2x[x] + self.vertex_dual_2x[y] - 2 * w
|
||||||
|
|
||||||
def delta2_add_edge(self, e: int, y: int, by: _Blossom) -> None:
|
def delta2_add_edge(self, e: int, y: int, by: Blossom) -> None:
|
||||||
"""Add edge "e" for delta2 tracking.
|
"""Add edge "e" for delta2 tracking.
|
||||||
|
|
||||||
Edge "e" connects an S-vertex to a T-vertex or unlabeled vertex "y".
|
Edge "e" connects an S-vertex to a T-vertex or unlabeled vertex "y".
|
||||||
|
@ -674,14 +674,14 @@ class _MatchingContext:
|
||||||
|
|
||||||
# If the blossom is unlabeled and the new edge becomes its least-slack
|
# If the blossom is unlabeled and the new edge becomes its least-slack
|
||||||
# S-edge, insert or update the blossom in the global delta2 queue.
|
# S-edge, insert or update the blossom in the global delta2 queue.
|
||||||
if by.label == _LABEL_NONE:
|
if by.label == LABEL_NONE:
|
||||||
prio += by.vertex_dual_offset
|
prio += by.vertex_dual_offset
|
||||||
if by.delta2_node is None:
|
if by.delta2_node is None:
|
||||||
by.delta2_node = self.delta2_queue.insert(prio, by)
|
by.delta2_node = self.delta2_queue.insert(prio, by)
|
||||||
elif prio < by.delta2_node.prio:
|
elif prio < by.delta2_node.prio:
|
||||||
self.delta2_queue.decrease_prio(by.delta2_node, prio)
|
self.delta2_queue.decrease_prio(by.delta2_node, prio)
|
||||||
|
|
||||||
def delta2_remove_edge(self, e: int, y: int, by: _Blossom) -> None:
|
def delta2_remove_edge(self, e: int, y: int, by: Blossom) -> None:
|
||||||
"""Remove edge "e" from delta2 tracking.
|
"""Remove edge "e" from delta2 tracking.
|
||||||
|
|
||||||
This function is called if an S-vertex becomes unlabeled,
|
This function is called if an S-vertex becomes unlabeled,
|
||||||
|
@ -705,7 +705,7 @@ class _MatchingContext:
|
||||||
# If necessary, update the priority of "y" in its UnionFindQueue.
|
# If necessary, update the priority of "y" in its UnionFindQueue.
|
||||||
if prio > self.vertex_set_node[y].prio:
|
if prio > self.vertex_set_node[y].prio:
|
||||||
self.vertex_set_node[y].set_prio(prio)
|
self.vertex_set_node[y].set_prio(prio)
|
||||||
if by.label == _LABEL_NONE:
|
if by.label == LABEL_NONE:
|
||||||
# Update or delete the blossom in the global delta2 queue.
|
# Update or delete the blossom in the global delta2 queue.
|
||||||
assert by.delta2_node is not None
|
assert by.delta2_node is not None
|
||||||
prio = by.vertex_set.min_prio()
|
prio = by.vertex_set.min_prio()
|
||||||
|
@ -718,7 +718,7 @@ class _MatchingContext:
|
||||||
self.delta2_queue.delete(by.delta2_node)
|
self.delta2_queue.delete(by.delta2_node)
|
||||||
by.delta2_node = None
|
by.delta2_node = None
|
||||||
|
|
||||||
def delta2_enable_blossom(self, blossom: _Blossom) -> None:
|
def delta2_enable_blossom(self, blossom: Blossom) -> None:
|
||||||
"""Enable delta2 tracking for "blossom".
|
"""Enable delta2 tracking for "blossom".
|
||||||
|
|
||||||
This function is called when a blossom becomes an unlabeled top-level
|
This function is called when a blossom becomes an unlabeled top-level
|
||||||
|
@ -733,7 +733,7 @@ class _MatchingContext:
|
||||||
prio += blossom.vertex_dual_offset
|
prio += blossom.vertex_dual_offset
|
||||||
blossom.delta2_node = self.delta2_queue.insert(prio, blossom)
|
blossom.delta2_node = self.delta2_queue.insert(prio, blossom)
|
||||||
|
|
||||||
def delta2_disable_blossom(self, blossom: _Blossom) -> None:
|
def delta2_disable_blossom(self, blossom: Blossom) -> None:
|
||||||
"""Disable delta2 tracking for "blossom".
|
"""Disable delta2 tracking for "blossom".
|
||||||
|
|
||||||
The blossom will be removed from the global delta2 queue.
|
The blossom will be removed from the global delta2 queue.
|
||||||
|
@ -780,7 +780,7 @@ class _MatchingContext:
|
||||||
prio = delta2_node.prio
|
prio = delta2_node.prio
|
||||||
slack_2x = prio - self.delta_sum_2x
|
slack_2x = prio - self.delta_sum_2x
|
||||||
assert blossom.parent is None
|
assert blossom.parent is None
|
||||||
assert blossom.label == _LABEL_NONE
|
assert blossom.label == LABEL_NONE
|
||||||
|
|
||||||
x = blossom.vertex_set.min_elem()
|
x = blossom.vertex_set.min_elem()
|
||||||
e = self.vertex_sedge_queue[x].find_min().data
|
e = self.vertex_sedge_queue[x].find_min().data
|
||||||
|
@ -840,7 +840,7 @@ class _MatchingContext:
|
||||||
(x, y, _w) = self.graph.edges[e]
|
(x, y, _w) = self.graph.edges[e]
|
||||||
bx = self.vertex_set_node[x].find()
|
bx = self.vertex_set_node[x].find()
|
||||||
by = self.vertex_set_node[y].find()
|
by = self.vertex_set_node[y].find()
|
||||||
assert (bx.label == _LABEL_S) and (by.label == _LABEL_S)
|
assert (bx.label == LABEL_S) and (by.label == LABEL_S)
|
||||||
if bx is not by:
|
if bx is not by:
|
||||||
slack = delta3_node.prio - self.delta_sum_2x
|
slack = delta3_node.prio - self.delta_sum_2x
|
||||||
return (e, slack)
|
return (e, slack)
|
||||||
|
@ -859,7 +859,7 @@ class _MatchingContext:
|
||||||
# Managing blossom labels:
|
# Managing blossom labels:
|
||||||
#
|
#
|
||||||
|
|
||||||
def assign_blossom_label_s(self, blossom: _Blossom) -> None:
|
def assign_blossom_label_s(self, blossom: Blossom) -> None:
|
||||||
"""Change an unlabeled top-level blossom into an S-blossom.
|
"""Change an unlabeled top-level blossom into an S-blossom.
|
||||||
|
|
||||||
For a blossom with "j" vertices and "k" incident edges,
|
For a blossom with "j" vertices and "k" incident edges,
|
||||||
|
@ -870,8 +870,8 @@ class _MatchingContext:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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.
|
# Labeled blossoms must not be in the delta2 queue.
|
||||||
self.delta2_disable_blossom(blossom)
|
self.delta2_disable_blossom(blossom)
|
||||||
|
@ -887,7 +887,7 @@ class _MatchingContext:
|
||||||
# The value of blossom.dual_var must be adjusted accordingly
|
# The value of blossom.dual_var must be adjusted accordingly
|
||||||
# when the blossom changes from unlabeled to S-blossom.
|
# 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
|
||||||
|
|
||||||
# Apply pending updates to vertex dual variables and prepare
|
# Apply pending updates to vertex dual variables and prepare
|
||||||
|
@ -916,20 +916,20 @@ class _MatchingContext:
|
||||||
# Add the new S-vertices to the scan queue.
|
# Add the new S-vertices to the scan queue.
|
||||||
self.scan_queue.extend(vertices)
|
self.scan_queue.extend(vertices)
|
||||||
|
|
||||||
def assign_blossom_label_t(self, blossom: _Blossom) -> None:
|
def assign_blossom_label_t(self, blossom: Blossom) -> None:
|
||||||
"""Change an unlabeled top-level blossom into a T-blossom.
|
"""Change an unlabeled top-level blossom into a T-blossom.
|
||||||
|
|
||||||
This function takes time O(log(n)).
|
This function takes time O(log(n)).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
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.
|
# 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):
|
||||||
|
|
||||||
# Adjust for lazy updating of T-blossom dual variables.
|
# Adjust for lazy updating of T-blossom dual variables.
|
||||||
#
|
#
|
||||||
|
@ -962,7 +962,7 @@ class _MatchingContext:
|
||||||
#
|
#
|
||||||
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:
|
def remove_blossom_label_s(self, blossom: Blossom) -> None:
|
||||||
"""Change a top-level S-blossom into an unlabeled blossom.
|
"""Change a top-level S-blossom into an unlabeled blossom.
|
||||||
|
|
||||||
For a blossom with "j" vertices and "k" incident edges,
|
For a blossom with "j" vertices and "k" incident edges,
|
||||||
|
@ -973,11 +973,11 @@ class _MatchingContext:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
assert blossom.parent is None
|
assert blossom.parent is None
|
||||||
assert blossom.label == _LABEL_S
|
assert blossom.label == LABEL_S
|
||||||
blossom.label = _LABEL_NONE
|
blossom.label = LABEL_NONE
|
||||||
|
|
||||||
# Unwind lazy delta updates to the S-blossom dual variable.
|
# Unwind lazy delta updates to the S-blossom dual variable.
|
||||||
if isinstance(blossom, _NonTrivialBlossom):
|
if isinstance(blossom, NonTrivialBlossom):
|
||||||
blossom.dual_var += self.delta_sum_2x
|
blossom.dual_var += self.delta_sum_2x
|
||||||
|
|
||||||
assert blossom.vertex_dual_offset == 0
|
assert blossom.vertex_dual_offset == 0
|
||||||
|
@ -1002,7 +1002,7 @@ class _MatchingContext:
|
||||||
self.delta3_remove_edge(e)
|
self.delta3_remove_edge(e)
|
||||||
|
|
||||||
by = self.vertex_set_node[y].find()
|
by = self.vertex_set_node[y].find()
|
||||||
if by.label == _LABEL_S:
|
if by.label == LABEL_S:
|
||||||
# Edge "e" connects unlabeled vertex "x" to S-vertex "y".
|
# Edge "e" connects unlabeled vertex "x" to S-vertex "y".
|
||||||
# It must be tracked for delta2 via vertex "x".
|
# It must be tracked for delta2 via vertex "x".
|
||||||
self.delta2_add_edge(e, x, blossom)
|
self.delta2_add_edge(e, x, blossom)
|
||||||
|
@ -1013,17 +1013,17 @@ class _MatchingContext:
|
||||||
# removed now.
|
# removed now.
|
||||||
self.delta2_remove_edge(e, y, by)
|
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:
|
||||||
"""Change a top-level T-blossom into an unlabeled blossom.
|
"""Change a top-level T-blossom into an unlabeled blossom.
|
||||||
|
|
||||||
This function takes time O(log(n)).
|
This function takes time O(log(n)).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
assert blossom.parent is None
|
assert blossom.parent is None
|
||||||
assert blossom.label == _LABEL_T
|
assert blossom.label == LABEL_T
|
||||||
blossom.label = _LABEL_NONE
|
blossom.label = LABEL_NONE
|
||||||
|
|
||||||
if isinstance(blossom, _NonTrivialBlossom):
|
if isinstance(blossom, NonTrivialBlossom):
|
||||||
|
|
||||||
# Unlabeled blossoms are not tracked in the 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
|
||||||
|
@ -1039,31 +1039,31 @@ class _MatchingContext:
|
||||||
# Enable unlabeled top-level blossom for delta2 tracking.
|
# Enable unlabeled top-level blossom for delta2 tracking.
|
||||||
self.delta2_enable_blossom(blossom)
|
self.delta2_enable_blossom(blossom)
|
||||||
|
|
||||||
def change_s_blossom_to_subblossom(self, blossom: _Blossom) -> None:
|
def change_s_blossom_to_subblossom(self, blossom: Blossom) -> None:
|
||||||
"""Change a top-level S-blossom into an S-subblossom.
|
"""Change a top-level S-blossom into an S-subblossom.
|
||||||
|
|
||||||
This function takes time O(1).
|
This function takes time O(1).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
assert blossom.parent is None
|
assert blossom.parent is None
|
||||||
assert blossom.label == _LABEL_S
|
assert blossom.label == LABEL_S
|
||||||
blossom.label = _LABEL_NONE
|
blossom.label = LABEL_NONE
|
||||||
|
|
||||||
# Unwind lazy delta updates to the S-blossom dual variable.
|
# Unwind lazy delta updates to the S-blossom dual variable.
|
||||||
if isinstance(blossom, _NonTrivialBlossom):
|
if isinstance(blossom, NonTrivialBlossom):
|
||||||
blossom.dual_var += self.delta_sum_2x
|
blossom.dual_var += self.delta_sum_2x
|
||||||
|
|
||||||
#
|
#
|
||||||
# General support routines:
|
# General support routines:
|
||||||
#
|
#
|
||||||
|
|
||||||
def reset_blossom_label(self, blossom: _Blossom) -> None:
|
def reset_blossom_label(self, blossom: Blossom) -> None:
|
||||||
"""Remove blossom label."""
|
"""Remove blossom label."""
|
||||||
|
|
||||||
assert blossom.parent is None
|
assert blossom.parent is None
|
||||||
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)
|
self.remove_blossom_label_s(blossom)
|
||||||
else:
|
else:
|
||||||
self.remove_blossom_label_t(blossom)
|
self.remove_blossom_label_t(blossom)
|
||||||
|
@ -1072,7 +1072,7 @@ class _MatchingContext:
|
||||||
"""TODO -- remove this function, only for debugging"""
|
"""TODO -- remove this function, only for debugging"""
|
||||||
for blossom in itertools.chain(self.trivial_blossom,
|
for blossom in itertools.chain(self.trivial_blossom,
|
||||||
self.nontrivial_blossom):
|
self.nontrivial_blossom):
|
||||||
if (blossom.parent is None) and (blossom.label != _LABEL_NONE):
|
if (blossom.parent is None) and (blossom.label != LABEL_NONE):
|
||||||
assert blossom.tree_blossoms is not None
|
assert blossom.tree_blossoms is not None
|
||||||
assert blossom in blossom.tree_blossoms
|
assert blossom in blossom.tree_blossoms
|
||||||
if blossom.tree_edge is not None:
|
if blossom.tree_edge is not None:
|
||||||
|
@ -1084,7 +1084,7 @@ class _MatchingContext:
|
||||||
assert blossom.tree_edge is None
|
assert blossom.tree_edge is None
|
||||||
assert blossom.tree_blossoms is None
|
assert blossom.tree_blossoms is None
|
||||||
|
|
||||||
def remove_alternating_tree(self, tree_blossoms: set[_Blossom]) -> None:
|
def remove_alternating_tree(self, tree_blossoms: set[Blossom]) -> None:
|
||||||
"""Reset the alternating tree consisting of the specified blossoms.
|
"""Reset the alternating tree consisting of the specified blossoms.
|
||||||
|
|
||||||
Marks the blossoms as unlabeled.
|
Marks the blossoms as unlabeled.
|
||||||
|
@ -1093,13 +1093,13 @@ class _MatchingContext:
|
||||||
This function takes time O((n + m) * log(n)).
|
This function takes time O((n + m) * log(n)).
|
||||||
"""
|
"""
|
||||||
for blossom in tree_blossoms:
|
for blossom in tree_blossoms:
|
||||||
assert blossom.label != _LABEL_NONE
|
assert blossom.label != LABEL_NONE
|
||||||
assert blossom.tree_blossoms is tree_blossoms
|
assert blossom.tree_blossoms is tree_blossoms
|
||||||
self.reset_blossom_label(blossom)
|
self.reset_blossom_label(blossom)
|
||||||
blossom.tree_edge = None
|
blossom.tree_edge = None
|
||||||
blossom.tree_blossoms = None
|
blossom.tree_blossoms = None
|
||||||
|
|
||||||
def trace_alternating_paths(self, x: int, y: int) -> _AlternatingPath:
|
def trace_alternating_paths(self, x: int, y: int) -> AlternatingPath:
|
||||||
"""Trace back through the alternating trees from vertices "x" and "y".
|
"""Trace back through the alternating trees from vertices "x" and "y".
|
||||||
|
|
||||||
If both vertices are part of the same alternating tree, this function
|
If both vertices are part of the same alternating tree, this function
|
||||||
|
@ -1119,7 +1119,7 @@ class _MatchingContext:
|
||||||
blossoms.
|
blossoms.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
marked_blossoms: list[_Blossom] = []
|
marked_blossoms: list[Blossom] = []
|
||||||
|
|
||||||
# "xedges" is a list of edges used while tracing from "x".
|
# "xedges" is a list of edges used while tracing from "x".
|
||||||
# "yedges" is a list of edges used while tracing from "y".
|
# "yedges" is a list of edges used while tracing from "y".
|
||||||
|
@ -1129,7 +1129,7 @@ class _MatchingContext:
|
||||||
|
|
||||||
# "first_common" is the first common ancestor of "x" and "y"
|
# "first_common" is the first common ancestor of "x" and "y"
|
||||||
# in the alternating tree, or None if there is no common ancestor.
|
# in the alternating tree, or None if there is no common ancestor.
|
||||||
first_common: Optional[_Blossom] = None
|
first_common: Optional[Blossom] = None
|
||||||
|
|
||||||
# Alternate between tracing the path from "x" and the path from "y".
|
# Alternate between tracing the path from "x" and the path from "y".
|
||||||
# This ensures that the search time is bounded by the size of the
|
# This ensures that the search time is bounded by the size of the
|
||||||
|
@ -1179,14 +1179,14 @@ class _MatchingContext:
|
||||||
# Any S-to-S alternating path must have odd length.
|
# Any S-to-S alternating path must have odd length.
|
||||||
assert len(path_edges) % 2 == 1
|
assert len(path_edges) % 2 == 1
|
||||||
|
|
||||||
return _AlternatingPath(edges=path_edges,
|
return AlternatingPath(edges=path_edges,
|
||||||
is_cycle=(first_common is not None))
|
is_cycle=(first_common is not None))
|
||||||
|
|
||||||
#
|
#
|
||||||
# Merge and expand blossoms:
|
# Merge and expand blossoms:
|
||||||
#
|
#
|
||||||
|
|
||||||
def make_blossom(self, path: _AlternatingPath) -> None:
|
def make_blossom(self, path: AlternatingPath) -> None:
|
||||||
"""Create a new blossom from an alternating cycle.
|
"""Create a new blossom from an alternating cycle.
|
||||||
|
|
||||||
Assign label S to the new blossom.
|
Assign label S to the new blossom.
|
||||||
|
@ -1214,21 +1214,21 @@ class _MatchingContext:
|
||||||
assert subblossoms[1:] == subblossoms_next[:-1]
|
assert subblossoms[1:] == subblossoms_next[:-1]
|
||||||
|
|
||||||
# Blossom must start and end with an S-sub-blossom.
|
# Blossom must start and end with an S-sub-blossom.
|
||||||
assert subblossoms[0].label == _LABEL_S
|
assert subblossoms[0].label == LABEL_S
|
||||||
|
|
||||||
# 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_T:
|
if sub.label == LABEL_T:
|
||||||
self.remove_blossom_label_t(sub)
|
self.remove_blossom_label_t(sub)
|
||||||
self.assign_blossom_label_s(sub)
|
self.assign_blossom_label_s(sub)
|
||||||
self.change_s_blossom_to_subblossom(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.
|
# Assign label S to the new blossom.
|
||||||
blossom.label = _LABEL_S
|
blossom.label = LABEL_S
|
||||||
|
|
||||||
# Prepare for lazy updating of S-blossom dual variable.
|
# Prepare for lazy updating of S-blossom dual variable.
|
||||||
blossom.dual_var = -self.delta_sum_2x
|
blossom.dual_var = -self.delta_sum_2x
|
||||||
|
@ -1257,9 +1257,9 @@ class _MatchingContext:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_path_through_blossom(
|
def find_path_through_blossom(
|
||||||
blossom: _NonTrivialBlossom,
|
blossom: NonTrivialBlossom,
|
||||||
sub: _Blossom
|
sub: Blossom
|
||||||
) -> tuple[list[_Blossom], list[tuple[int, int]]]:
|
) -> tuple[list[Blossom], list[tuple[int, int]]]:
|
||||||
"""Construct a path with an even number of edges through the
|
"""Construct a path with an even number of edges through the
|
||||||
specified blossom, from sub-blossom "sub" to the base of "blossom".
|
specified blossom, from sub-blossom "sub" to the base of "blossom".
|
||||||
|
|
||||||
|
@ -1285,14 +1285,14 @@ class _MatchingContext:
|
||||||
|
|
||||||
return (nodes, edges)
|
return (nodes, edges)
|
||||||
|
|
||||||
def expand_unlabeled_blossom(self, blossom: _NonTrivialBlossom) -> None:
|
def expand_unlabeled_blossom(self, blossom: NonTrivialBlossom) -> None:
|
||||||
"""Expand the specified unlabeled blossom.
|
"""Expand the specified unlabeled blossom.
|
||||||
|
|
||||||
This function takes total time O(n * log(n)) per stage.
|
This function takes total time O(n * log(n)) per stage.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
assert blossom.parent is None
|
assert blossom.parent is None
|
||||||
assert blossom.label == _LABEL_NONE
|
assert blossom.label == LABEL_NONE
|
||||||
|
|
||||||
# Remove blossom from the delta2 queue.
|
# Remove blossom from the delta2 queue.
|
||||||
self.delta2_disable_blossom(blossom)
|
self.delta2_disable_blossom(blossom)
|
||||||
|
@ -1306,7 +1306,7 @@ class _MatchingContext:
|
||||||
|
|
||||||
# Convert sub-blossoms into top-level blossoms.
|
# Convert sub-blossoms into top-level blossoms.
|
||||||
for sub in blossom.subblossoms:
|
for sub in blossom.subblossoms:
|
||||||
assert sub.label == _LABEL_NONE
|
assert sub.label == LABEL_NONE
|
||||||
sub.parent = None
|
sub.parent = None
|
||||||
|
|
||||||
assert sub.vertex_dual_offset == 0
|
assert sub.vertex_dual_offset == 0
|
||||||
|
@ -1320,14 +1320,14 @@ class _MatchingContext:
|
||||||
# Delete the expanded blossom.
|
# Delete the expanded blossom.
|
||||||
self.nontrivial_blossom.remove(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.
|
||||||
|
|
||||||
This function takes total time O(n * log(n) + m) per stage.
|
This function takes total time O(n * log(n) + m) per stage.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
assert blossom.parent is None
|
assert blossom.parent is None
|
||||||
assert blossom.label == _LABEL_T
|
assert blossom.label == LABEL_T
|
||||||
assert blossom.delta2_node is None
|
assert blossom.delta2_node is None
|
||||||
|
|
||||||
# Remove blossom from its alternating tree.
|
# Remove blossom from its alternating tree.
|
||||||
|
@ -1391,9 +1391,9 @@ class _MatchingContext:
|
||||||
|
|
||||||
def augment_blossom_rec(
|
def augment_blossom_rec(
|
||||||
self,
|
self,
|
||||||
blossom: _NonTrivialBlossom,
|
blossom: NonTrivialBlossom,
|
||||||
sub: _Blossom,
|
sub: Blossom,
|
||||||
stack: list[tuple[_NonTrivialBlossom, _Blossom]]
|
stack: list[tuple[NonTrivialBlossom, Blossom]]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Augment along an alternating path through the specified blossom,
|
"""Augment along an alternating path through the specified blossom,
|
||||||
from sub-blossom "sub" to the base vertex of the blossom.
|
from sub-blossom "sub" to the base vertex of the blossom.
|
||||||
|
@ -1430,11 +1430,11 @@ class _MatchingContext:
|
||||||
# Augment through the subblossoms touching the edge (x, y).
|
# Augment through the subblossoms touching the edge (x, y).
|
||||||
# Nothing needs to be done for trivial subblossoms.
|
# Nothing needs to be done for trivial subblossoms.
|
||||||
bx = path_nodes[p+1]
|
bx = path_nodes[p+1]
|
||||||
if isinstance(bx, _NonTrivialBlossom):
|
if isinstance(bx, NonTrivialBlossom):
|
||||||
stack.append((bx, self.trivial_blossom[x]))
|
stack.append((bx, self.trivial_blossom[x]))
|
||||||
|
|
||||||
by = path_nodes[p+2]
|
by = path_nodes[p+2]
|
||||||
if isinstance(by, _NonTrivialBlossom):
|
if isinstance(by, NonTrivialBlossom):
|
||||||
stack.append((by, self.trivial_blossom[y]))
|
stack.append((by, self.trivial_blossom[y]))
|
||||||
|
|
||||||
# Rotate the subblossom list so the new base ends up in position 0.
|
# Rotate the subblossom list so the new base ends up in position 0.
|
||||||
|
@ -1450,8 +1450,8 @@ class _MatchingContext:
|
||||||
|
|
||||||
def augment_blossom(
|
def augment_blossom(
|
||||||
self,
|
self,
|
||||||
blossom: _NonTrivialBlossom,
|
blossom: NonTrivialBlossom,
|
||||||
sub: _Blossom
|
sub: Blossom
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Augment along an alternating path through the specified blossom,
|
"""Augment along an alternating path through the specified blossom,
|
||||||
from sub-blossom "sub" to the base vertex of the blossom.
|
from sub-blossom "sub" to the base vertex of the blossom.
|
||||||
|
@ -1486,7 +1486,7 @@ class _MatchingContext:
|
||||||
# Augment "blossom" from "sub" to the base vertex.
|
# Augment "blossom" from "sub" to the base vertex.
|
||||||
self.augment_blossom_rec(blossom, sub, stack)
|
self.augment_blossom_rec(blossom, sub, stack)
|
||||||
|
|
||||||
def augment_matching(self, path: _AlternatingPath) -> None:
|
def augment_matching(self, path: AlternatingPath) -> None:
|
||||||
"""Augment the matching through the specified augmenting path.
|
"""Augment the matching through the specified augmenting path.
|
||||||
|
|
||||||
This function takes time O(n).
|
This function takes time O(n).
|
||||||
|
@ -1515,11 +1515,11 @@ class _MatchingContext:
|
||||||
# Augment the non-trivial blossoms on either side of this edge.
|
# Augment the non-trivial blossoms on either side of this edge.
|
||||||
# No action is necessary for trivial blossoms.
|
# No action is necessary for trivial blossoms.
|
||||||
bx = self.vertex_set_node[x].find()
|
bx = self.vertex_set_node[x].find()
|
||||||
if isinstance(bx, _NonTrivialBlossom):
|
if isinstance(bx, NonTrivialBlossom):
|
||||||
self.augment_blossom(bx, self.trivial_blossom[x])
|
self.augment_blossom(bx, self.trivial_blossom[x])
|
||||||
|
|
||||||
by = self.vertex_set_node[y].find()
|
by = self.vertex_set_node[y].find()
|
||||||
if isinstance(by, _NonTrivialBlossom):
|
if isinstance(by, NonTrivialBlossom):
|
||||||
self.augment_blossom(by, self.trivial_blossom[y])
|
self.augment_blossom(by, self.trivial_blossom[y])
|
||||||
|
|
||||||
# Pull the edge into the matching.
|
# Pull the edge into the matching.
|
||||||
|
@ -1551,7 +1551,7 @@ class _MatchingContext:
|
||||||
assert y != -1
|
assert y != -1
|
||||||
|
|
||||||
by = self.vertex_set_node[y].find()
|
by = self.vertex_set_node[y].find()
|
||||||
assert by.label == _LABEL_T
|
assert by.label == LABEL_T
|
||||||
assert by.tree_blossoms is not None
|
assert by.tree_blossoms is not None
|
||||||
|
|
||||||
# Attach the blossom that contains "x" to the alternating tree.
|
# Attach the blossom that contains "x" to the alternating tree.
|
||||||
|
@ -1575,10 +1575,10 @@ class _MatchingContext:
|
||||||
|
|
||||||
bx = self.vertex_set_node[x].find()
|
bx = self.vertex_set_node[x].find()
|
||||||
by = self.vertex_set_node[y].find()
|
by = self.vertex_set_node[y].find()
|
||||||
assert bx.label == _LABEL_S
|
assert bx.label == LABEL_S
|
||||||
|
|
||||||
# Expand zero-dual blossoms before assigning label T.
|
# Expand zero-dual blossoms before assigning label T.
|
||||||
while isinstance(by, _NonTrivialBlossom) and (by.dual_var == 0):
|
while isinstance(by, NonTrivialBlossom) and (by.dual_var == 0):
|
||||||
self.expand_unlabeled_blossom(by)
|
self.expand_unlabeled_blossom(by)
|
||||||
by = self.vertex_set_node[y].find()
|
by = self.vertex_set_node[y].find()
|
||||||
|
|
||||||
|
@ -1617,8 +1617,8 @@ class _MatchingContext:
|
||||||
bx = self.vertex_set_node[x].find()
|
bx = self.vertex_set_node[x].find()
|
||||||
by = self.vertex_set_node[y].find()
|
by = self.vertex_set_node[y].find()
|
||||||
|
|
||||||
assert bx.label == _LABEL_S
|
assert bx.label == LABEL_S
|
||||||
assert by.label == _LABEL_S
|
assert by.label == LABEL_S
|
||||||
assert bx is not by
|
assert bx is not by
|
||||||
|
|
||||||
# Trace back through the alternating trees from "x" and "y".
|
# Trace back through the alternating trees from "x" and "y".
|
||||||
|
@ -1680,7 +1680,7 @@ class _MatchingContext:
|
||||||
|
|
||||||
# Double-check that "x" is an S-vertex.
|
# Double-check that "x" is an S-vertex.
|
||||||
bx = self.vertex_set_node[x].find()
|
bx = self.vertex_set_node[x].find()
|
||||||
assert bx.label == _LABEL_S
|
assert bx.label == LABEL_S
|
||||||
|
|
||||||
# Scan the edges that are incident on "x".
|
# Scan the edges that are incident on "x".
|
||||||
# This loop runs through O(m) iterations per stage.
|
# This loop runs through O(m) iterations per stage.
|
||||||
|
@ -1703,7 +1703,7 @@ class _MatchingContext:
|
||||||
if bx is by:
|
if bx is by:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if by.label == _LABEL_S:
|
if by.label == LABEL_S:
|
||||||
self.delta3_add_edge(e)
|
self.delta3_add_edge(e)
|
||||||
else:
|
else:
|
||||||
self.delta2_add_edge(e, y, by)
|
self.delta2_add_edge(e, y, by)
|
||||||
|
@ -1716,7 +1716,7 @@ class _MatchingContext:
|
||||||
|
|
||||||
def calc_dual_delta_step(
|
def calc_dual_delta_step(
|
||||||
self
|
self
|
||||||
) -> tuple[int, float, int, Optional[_NonTrivialBlossom]]:
|
) -> tuple[int, float, int, Optional[NonTrivialBlossom]]:
|
||||||
"""Calculate a delta step in the dual LPP problem.
|
"""Calculate a delta step in the dual LPP problem.
|
||||||
|
|
||||||
This function returns the minimum of the 4 types of delta values,
|
This function returns the minimum of the 4 types of delta values,
|
||||||
|
@ -1740,7 +1740,7 @@ class _MatchingContext:
|
||||||
Tuple (delta_type, delta_2x, delta_edge, delta_blossom).
|
Tuple (delta_type, delta_2x, delta_edge, delta_blossom).
|
||||||
"""
|
"""
|
||||||
delta_edge = -1
|
delta_edge = -1
|
||||||
delta_blossom: Optional[_NonTrivialBlossom] = None
|
delta_blossom: Optional[NonTrivialBlossom] = None
|
||||||
|
|
||||||
# Compute delta1: minimum dual variable of any S-vertex.
|
# Compute delta1: minimum dual variable of any S-vertex.
|
||||||
# All unmatched vertices have the same dual value, and this is
|
# All unmatched vertices have the same dual value, and this is
|
||||||
|
@ -1770,7 +1770,7 @@ class _MatchingContext:
|
||||||
# This takes time O(log(n)).
|
# This takes time O(log(n)).
|
||||||
if not self.delta4_queue.empty():
|
if not self.delta4_queue.empty():
|
||||||
blossom = self.delta4_queue.find_min().data
|
blossom = self.delta4_queue.find_min().data
|
||||||
assert blossom.label == _LABEL_T
|
assert blossom.label == LABEL_T
|
||||||
assert blossom.parent is None
|
assert blossom.parent is None
|
||||||
blossom_dual = blossom.dual_var - self.delta_sum_2x
|
blossom_dual = blossom.dual_var - self.delta_sum_2x
|
||||||
if blossom_dual <= delta_2x:
|
if blossom_dual <= delta_2x:
|
||||||
|
@ -1847,7 +1847,7 @@ class _MatchingContext:
|
||||||
# Use the edge from S-vertex to unlabeled vertex that got
|
# Use the edge from S-vertex to unlabeled vertex that got
|
||||||
# unlocked through the delta update.
|
# unlocked through the delta update.
|
||||||
(x, y, _w) = self.graph.edges[delta_edge]
|
(x, y, _w) = self.graph.edges[delta_edge]
|
||||||
if self.vertex_set_node[x].find().label != _LABEL_S:
|
if self.vertex_set_node[x].find().label != LABEL_S:
|
||||||
(x, y) = (y, x)
|
(x, y) = (y, x)
|
||||||
self.extend_tree_s_to_t(x, y)
|
self.extend_tree_s_to_t(x, y)
|
||||||
|
|
||||||
|
@ -1886,9 +1886,9 @@ class _MatchingContext:
|
||||||
self.nontrivial_blossom):
|
self.nontrivial_blossom):
|
||||||
|
|
||||||
# Remove blossom label.
|
# Remove blossom label.
|
||||||
if (blossom.parent is None) and (blossom.label != _LABEL_NONE):
|
if (blossom.parent is None) and (blossom.label != LABEL_NONE):
|
||||||
self.reset_blossom_label(blossom)
|
self.reset_blossom_label(blossom)
|
||||||
assert blossom.label == _LABEL_NONE
|
assert blossom.label == LABEL_NONE
|
||||||
|
|
||||||
# Remove blossom from alternating tree.
|
# Remove blossom from alternating tree.
|
||||||
blossom.tree_edge = None
|
blossom.tree_edge = None
|
||||||
|
@ -1906,8 +1906,8 @@ class _MatchingContext:
|
||||||
|
|
||||||
|
|
||||||
def _verify_blossom_edges(
|
def _verify_blossom_edges(
|
||||||
ctx: _MatchingContext,
|
ctx: MatchingContext,
|
||||||
blossom: _NonTrivialBlossom,
|
blossom: NonTrivialBlossom,
|
||||||
edge_slack_2x: list[float]
|
edge_slack_2x: list[float]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Descend down the blossom tree to find edges that are contained
|
"""Descend down the blossom tree to find edges that are contained
|
||||||
|
@ -1943,7 +1943,7 @@ def _verify_blossom_edges(
|
||||||
path_num_matched: list[int] = [0]
|
path_num_matched: list[int] = [0]
|
||||||
|
|
||||||
# Use an explicit stack to avoid deep recursion.
|
# Use an explicit stack to avoid deep recursion.
|
||||||
stack: list[tuple[_NonTrivialBlossom, int]] = [(blossom, -1)]
|
stack: list[tuple[NonTrivialBlossom, int]] = [(blossom, -1)]
|
||||||
|
|
||||||
while stack:
|
while stack:
|
||||||
(blossom, p) = stack[-1]
|
(blossom, p) = stack[-1]
|
||||||
|
@ -1969,7 +1969,7 @@ def _verify_blossom_edges(
|
||||||
|
|
||||||
# Examine the next sub-blossom at the current level.
|
# Examine the next sub-blossom at the current level.
|
||||||
sub = blossom.subblossoms[p]
|
sub = blossom.subblossoms[p]
|
||||||
if isinstance(sub, _NonTrivialBlossom):
|
if isinstance(sub, NonTrivialBlossom):
|
||||||
# Prepare to descent into the selected sub-blossom and
|
# Prepare to descent into the selected sub-blossom and
|
||||||
# scan it recursively.
|
# scan it recursively.
|
||||||
stack.append((sub, -1))
|
stack.append((sub, -1))
|
||||||
|
@ -2031,7 +2031,7 @@ def _verify_blossom_edges(
|
||||||
stack.pop()
|
stack.pop()
|
||||||
|
|
||||||
|
|
||||||
def _verify_optimum(ctx: _MatchingContext) -> None:
|
def verify_optimum(ctx: MatchingContext) -> None:
|
||||||
"""Verify that the optimum solution has been found.
|
"""Verify that the optimum solution has been found.
|
||||||
|
|
||||||
This function takes time O(n**2).
|
This function takes time O(n**2).
|
|
@ -4,10 +4,12 @@ import math
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
import mwmatching
|
|
||||||
from mwmatching import (
|
from mwmatching import (
|
||||||
maximum_weight_matching as mwm,
|
maximum_weight_matching as mwm,
|
||||||
adjust_weights_for_maximum_cardinality_matching as adj)
|
adjust_weights_for_maximum_cardinality_matching as adj)
|
||||||
|
from mwmatching.algorithm import (
|
||||||
|
MatchingError, GraphInfo, Blossom, NonTrivialBlossom, MatchingContext,
|
||||||
|
verify_optimum)
|
||||||
|
|
||||||
|
|
||||||
class TestMaximumWeightMatching(unittest.TestCase):
|
class TestMaximumWeightMatching(unittest.TestCase):
|
||||||
|
@ -431,10 +433,10 @@ class TestMaximumCardinalityMatching(unittest.TestCase):
|
||||||
|
|
||||||
|
|
||||||
class TestGraphInfo(unittest.TestCase):
|
class TestGraphInfo(unittest.TestCase):
|
||||||
"""Test _GraphInfo helper class."""
|
"""Test GraphInfo helper class."""
|
||||||
|
|
||||||
def test_empty(self):
|
def test_empty(self):
|
||||||
graph = mwmatching._GraphInfo([])
|
graph = GraphInfo([])
|
||||||
self.assertEqual(graph.num_vertex, 0)
|
self.assertEqual(graph.num_vertex, 0)
|
||||||
self.assertEqual(graph.edges, [])
|
self.assertEqual(graph.edges, [])
|
||||||
self.assertEqual(graph.adjacent_edges, [])
|
self.assertEqual(graph.adjacent_edges, [])
|
||||||
|
@ -449,8 +451,8 @@ class TestVerificationFail(unittest.TestCase):
|
||||||
vertex_mate,
|
vertex_mate,
|
||||||
vertex_dual_2x,
|
vertex_dual_2x,
|
||||||
nontrivial_blossom):
|
nontrivial_blossom):
|
||||||
ctx = Mock(spec=mwmatching._MatchingContext)
|
ctx = Mock(spec=MatchingContext)
|
||||||
ctx.graph = mwmatching._GraphInfo(edges)
|
ctx.graph = GraphInfo(edges)
|
||||||
ctx.vertex_mate = vertex_mate
|
ctx.vertex_mate = vertex_mate
|
||||||
ctx.vertex_dual_2x = vertex_dual_2x
|
ctx.vertex_dual_2x = vertex_dual_2x
|
||||||
ctx.nontrivial_blossom = nontrivial_blossom
|
ctx.nontrivial_blossom = nontrivial_blossom
|
||||||
|
@ -463,7 +465,7 @@ class TestVerificationFail(unittest.TestCase):
|
||||||
vertex_mate=[-1, 2, 1],
|
vertex_mate=[-1, 2, 1],
|
||||||
vertex_dual_2x=[0, 20, 2],
|
vertex_dual_2x=[0, 20, 2],
|
||||||
nontrivial_blossom=[])
|
nontrivial_blossom=[])
|
||||||
mwmatching._verify_optimum(ctx)
|
verify_optimum(ctx)
|
||||||
|
|
||||||
def test_asymmetric_matching(self):
|
def test_asymmetric_matching(self):
|
||||||
edges = [(0,1,10), (1,2,11)]
|
edges = [(0,1,10), (1,2,11)]
|
||||||
|
@ -472,8 +474,8 @@ class TestVerificationFail(unittest.TestCase):
|
||||||
vertex_mate=[-1, 2, 0],
|
vertex_mate=[-1, 2, 0],
|
||||||
vertex_dual_2x=[0, 20, 2],
|
vertex_dual_2x=[0, 20, 2],
|
||||||
nontrivial_blossom=[])
|
nontrivial_blossom=[])
|
||||||
with self.assertRaises(mwmatching.MatchingError):
|
with self.assertRaises(MatchingError):
|
||||||
mwmatching._verify_optimum(ctx)
|
verify_optimum(ctx)
|
||||||
|
|
||||||
def test_nonexistent_matched_edge(self):
|
def test_nonexistent_matched_edge(self):
|
||||||
edges = [(0,1,10), (1,2,11)]
|
edges = [(0,1,10), (1,2,11)]
|
||||||
|
@ -482,8 +484,8 @@ class TestVerificationFail(unittest.TestCase):
|
||||||
vertex_mate=[2, -1, 0],
|
vertex_mate=[2, -1, 0],
|
||||||
vertex_dual_2x=[11, 11, 11],
|
vertex_dual_2x=[11, 11, 11],
|
||||||
nontrivial_blossom=[])
|
nontrivial_blossom=[])
|
||||||
with self.assertRaises(mwmatching.MatchingError):
|
with self.assertRaises(MatchingError):
|
||||||
mwmatching._verify_optimum(ctx)
|
verify_optimum(ctx)
|
||||||
|
|
||||||
def test_negative_vertex_dual(self):
|
def test_negative_vertex_dual(self):
|
||||||
edges = [(0,1,10), (1,2,11)]
|
edges = [(0,1,10), (1,2,11)]
|
||||||
|
@ -492,8 +494,8 @@ class TestVerificationFail(unittest.TestCase):
|
||||||
vertex_mate=[-1, 2, 1],
|
vertex_mate=[-1, 2, 1],
|
||||||
vertex_dual_2x=[-2, 22, 0],
|
vertex_dual_2x=[-2, 22, 0],
|
||||||
nontrivial_blossom=[])
|
nontrivial_blossom=[])
|
||||||
with self.assertRaises(mwmatching.MatchingError):
|
with self.assertRaises(MatchingError):
|
||||||
mwmatching._verify_optimum(ctx)
|
verify_optimum(ctx)
|
||||||
|
|
||||||
def test_unmatched_nonzero_dual(self):
|
def test_unmatched_nonzero_dual(self):
|
||||||
edges = [(0,1,10), (1,2,11)]
|
edges = [(0,1,10), (1,2,11)]
|
||||||
|
@ -502,8 +504,8 @@ class TestVerificationFail(unittest.TestCase):
|
||||||
vertex_mate=[-1, 2, 1],
|
vertex_mate=[-1, 2, 1],
|
||||||
vertex_dual_2x=[9, 11, 11],
|
vertex_dual_2x=[9, 11, 11],
|
||||||
nontrivial_blossom=[])
|
nontrivial_blossom=[])
|
||||||
with self.assertRaises(mwmatching.MatchingError):
|
with self.assertRaises(MatchingError):
|
||||||
mwmatching._verify_optimum(ctx)
|
verify_optimum(ctx)
|
||||||
|
|
||||||
def test_negative_edge_slack(self):
|
def test_negative_edge_slack(self):
|
||||||
edges = [(0,1,10), (1,2,11)]
|
edges = [(0,1,10), (1,2,11)]
|
||||||
|
@ -512,8 +514,8 @@ class TestVerificationFail(unittest.TestCase):
|
||||||
vertex_mate=[-1, 2, 1],
|
vertex_mate=[-1, 2, 1],
|
||||||
vertex_dual_2x=[0, 11, 11],
|
vertex_dual_2x=[0, 11, 11],
|
||||||
nontrivial_blossom=[])
|
nontrivial_blossom=[])
|
||||||
with self.assertRaises(mwmatching.MatchingError):
|
with self.assertRaises(MatchingError):
|
||||||
mwmatching._verify_optimum(ctx)
|
verify_optimum(ctx)
|
||||||
|
|
||||||
def test_matched_edge_slack(self):
|
def test_matched_edge_slack(self):
|
||||||
edges = [(0,1,10), (1,2,11)]
|
edges = [(0,1,10), (1,2,11)]
|
||||||
|
@ -522,8 +524,8 @@ class TestVerificationFail(unittest.TestCase):
|
||||||
vertex_mate=[-1, 2, 1],
|
vertex_mate=[-1, 2, 1],
|
||||||
vertex_dual_2x=[0, 20, 11],
|
vertex_dual_2x=[0, 20, 11],
|
||||||
nontrivial_blossom=[])
|
nontrivial_blossom=[])
|
||||||
with self.assertRaises(mwmatching.MatchingError):
|
with self.assertRaises(MatchingError):
|
||||||
mwmatching._verify_optimum(ctx)
|
verify_optimum(ctx)
|
||||||
|
|
||||||
def test_negative_blossom_dual(self):
|
def test_negative_blossom_dual(self):
|
||||||
#
|
#
|
||||||
|
@ -532,11 +534,8 @@ class TestVerificationFail(unittest.TestCase):
|
||||||
# \----8-----/
|
# \----8-----/
|
||||||
#
|
#
|
||||||
edges = [(0,1,7), (0,2,8), (1,2,9), (2,3,6)]
|
edges = [(0,1,7), (0,2,8), (1,2,9), (2,3,6)]
|
||||||
blossom = mwmatching._NonTrivialBlossom(
|
blossom = NonTrivialBlossom(
|
||||||
subblossoms=[
|
subblossoms=[Blossom(0), Blossom(1), Blossom(2)],
|
||||||
mwmatching._Blossom(0),
|
|
||||||
mwmatching._Blossom(1),
|
|
||||||
mwmatching._Blossom(2)],
|
|
||||||
edges=[0,2,1])
|
edges=[0,2,1])
|
||||||
for sub in blossom.subblossoms:
|
for sub in blossom.subblossoms:
|
||||||
sub.parent = blossom
|
sub.parent = blossom
|
||||||
|
@ -546,8 +545,8 @@ class TestVerificationFail(unittest.TestCase):
|
||||||
vertex_mate=[1, 0, 3, 2],
|
vertex_mate=[1, 0, 3, 2],
|
||||||
vertex_dual_2x=[4, 6, 8, 4],
|
vertex_dual_2x=[4, 6, 8, 4],
|
||||||
nontrivial_blossom=[blossom])
|
nontrivial_blossom=[blossom])
|
||||||
with self.assertRaises(mwmatching.MatchingError):
|
with self.assertRaises(MatchingError):
|
||||||
mwmatching._verify_optimum(ctx)
|
verify_optimum(ctx)
|
||||||
|
|
||||||
def test_blossom_not_full(self):
|
def test_blossom_not_full(self):
|
||||||
#
|
#
|
||||||
|
@ -560,11 +559,8 @@ class TestVerificationFail(unittest.TestCase):
|
||||||
# \----2-----/
|
# \----2-----/
|
||||||
#
|
#
|
||||||
edges = [(0,1,7), (0,2,2), (1,2,5), (0,3,8), (1,4,8)]
|
edges = [(0,1,7), (0,2,2), (1,2,5), (0,3,8), (1,4,8)]
|
||||||
blossom = mwmatching._NonTrivialBlossom(
|
blossom = NonTrivialBlossom(
|
||||||
subblossoms=[
|
subblossoms=[Blossom(0), Blossom(1), Blossom(2)],
|
||||||
mwmatching._Blossom(0),
|
|
||||||
mwmatching._Blossom(1),
|
|
||||||
mwmatching._Blossom(2)],
|
|
||||||
edges=[0,2,1])
|
edges=[0,2,1])
|
||||||
for sub in blossom.subblossoms:
|
for sub in blossom.subblossoms:
|
||||||
sub.parent = blossom
|
sub.parent = blossom
|
||||||
|
@ -574,8 +570,8 @@ class TestVerificationFail(unittest.TestCase):
|
||||||
vertex_mate=[3, 4, -1, 0, 1],
|
vertex_mate=[3, 4, -1, 0, 1],
|
||||||
vertex_dual_2x=[4, 10, 0, 12, 6],
|
vertex_dual_2x=[4, 10, 0, 12, 6],
|
||||||
nontrivial_blossom=[blossom])
|
nontrivial_blossom=[blossom])
|
||||||
with self.assertRaises(mwmatching.MatchingError):
|
with self.assertRaises(MatchingError):
|
||||||
mwmatching._verify_optimum(ctx)
|
verify_optimum(ctx)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
|
@ -3,7 +3,7 @@
|
||||||
import random
|
import random
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from datastruct import UnionFindQueue, PriorityQueue
|
from mwmatching.datastruct import UnionFindQueue, PriorityQueue
|
||||||
|
|
||||||
|
|
||||||
class TestUnionFindQueue(unittest.TestCase):
|
class TestUnionFindQueue(unittest.TestCase):
|
|
@ -4,7 +4,7 @@ set -e
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Running pycodestyle"
|
echo "Running pycodestyle"
|
||||||
pycodestyle python/mwmatching.py python/datastruct.py tests
|
pycodestyle python/mwmatching python/run_matching.py tests
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Running mypy"
|
echo "Running mypy"
|
||||||
|
@ -12,20 +12,15 @@ mypy --disallow-incomplete-defs python tests
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Running pylint"
|
echo "Running pylint"
|
||||||
pylint --ignore=test_mwmatching.py python tests || [ $(($? & 3)) -eq 0 ]
|
pylint --ignore=test_algorithm.py python tests/*.py tests/generate/*.py || [ $(($? & 3)) -eq 0 ]
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Running test_mwmatching.py"
|
echo "Running unit tests"
|
||||||
python3 python/test_mwmatching.py
|
python3 -m unittest discover -t python -s python/tests
|
||||||
|
|
||||||
echo
|
|
||||||
echo "Running test_datastruct.py"
|
|
||||||
python3 python/test_datastruct.py
|
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Checking test coverage"
|
echo "Checking test coverage"
|
||||||
coverage erase
|
coverage erase
|
||||||
coverage run --branch python/test_datastruct.py
|
coverage run --branch -m unittest discover -t python -s python/tests
|
||||||
coverage run -a --branch python/test_mwmatching.py
|
|
||||||
coverage report -m
|
coverage report -m
|
||||||
|
|
||||||
|
|
|
@ -19,13 +19,12 @@ count_delta_step = [0]
|
||||||
|
|
||||||
def patch_matching_code() -> None:
|
def patch_matching_code() -> None:
|
||||||
"""Patch the matching code to count events."""
|
"""Patch the matching code to count events."""
|
||||||
# pylint: disable=import-outside-toplevel,protected-access
|
# pylint: disable=import-outside-toplevel
|
||||||
|
|
||||||
import mwmatching
|
from mwmatching.algorithm import MatchingContext
|
||||||
|
|
||||||
orig_make_blossom = mwmatching._MatchingContext.make_blossom
|
orig_make_blossom = MatchingContext.make_blossom
|
||||||
orig_calc_dual_delta_step = (
|
orig_calc_dual_delta_step = MatchingContext.calc_dual_delta_step
|
||||||
mwmatching._MatchingContext.calc_dual_delta_step)
|
|
||||||
|
|
||||||
def stub_make_blossom(*args: Any, **kwargs: Any) -> Any:
|
def stub_make_blossom(*args: Any, **kwargs: Any) -> Any:
|
||||||
count_make_blossom[0] += 1
|
count_make_blossom[0] += 1
|
||||||
|
@ -36,9 +35,8 @@ def patch_matching_code() -> None:
|
||||||
ret = orig_calc_dual_delta_step(*args, **kwargs)
|
ret = orig_calc_dual_delta_step(*args, **kwargs)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
mwmatching._MatchingContext.make_blossom = ( # type: ignore
|
MatchingContext.make_blossom = stub_make_blossom # type: ignore
|
||||||
stub_make_blossom)
|
MatchingContext.calc_dual_delta_step = ( # type: ignore
|
||||||
mwmatching._MatchingContext.calc_dual_delta_step = ( # type: ignore
|
|
||||||
stub_calc_dual_delta_step)
|
stub_calc_dual_delta_step)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue