org.jgrapht.alg.matching.blossom.v5.BlossomVDualUpdater Maven / Gradle / Ivy
/*
* (C) Copyright 2018-2023, by Timofey Chudakov and Contributors.
*
* JGraphT : a free Java graph-theory library
*
* See the CONTRIBUTORS.md file distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the
* GNU Lesser General Public License v2.1 or later
* which is available at
* http://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html.
*
* SPDX-License-Identifier: EPL-2.0 OR LGPL-2.1-or-later
*/
package org.jgrapht.alg.matching.blossom.v5;
import org.jheaps.*;
import static org.jgrapht.alg.matching.blossom.v5.BlossomVOptions.DualUpdateStrategy.MULTIPLE_TREE_CONNECTED_COMPONENTS;
import static org.jgrapht.alg.matching.blossom.v5.BlossomVOptions.DualUpdateStrategy.MULTIPLE_TREE_FIXED_DELTA;
import static org.jgrapht.alg.matching.blossom.v5.KolmogorovWeightedPerfectMatching.EPS;
import static org.jgrapht.alg.matching.blossom.v5.KolmogorovWeightedPerfectMatching.INFINITY;
/**
* This class is used by {@link KolmogorovWeightedPerfectMatching} to perform dual updates, thus
* increasing the dual objective function value and creating new tight edges.
*
* This class currently supports three types of dual updates: single tree, multiple trees fixed
* delta, and multiple tree variable delta. The first one is used to updates duals of a single tree,
* when at least one of the {@link BlossomVOptions#updateDualsBefore} or
* {@link BlossomVOptions#updateDualsAfter} is true. The latter two are used to update the duals
* globally and are defined by the {@link BlossomVOptions}.
*
* There are two type of constraints on a dual change of a tree: in-tree and cross-tree. In-tree
* constraints are imposed by the infinity edges, (+, +) in-tree edges and "-" blossoms. Cross-tree
* constraints are imposed by (+, +), (+, -) and (-, +) cross-tree edges. With respect to this
* classification of constraints the following strategies of changing the duals can be used:
*
* - Single tree strategy greedily increases the duals of the tree with respect to the in-tree and
* cross-tree constraints. This can result in a zero-change update. If a tight (+, +) cross-tree
* edge is encountered during this operation, an immediate augmentation is performed
* afterwards.
*
* - Multiple tree fixed delta approach considers only in-tree constraints and constraints imposed
* by the (+, +) cross-tree edges. Since this approach increases the trees' epsilons by the same
* amount, it doesn't need to consider other two dual constraints. If a tight (+, +) cross-tree edge
* is encountered during this operation, an immediate augmentation is performed afterwards.
*
* - Multiple tree variable delta approach considers all types of constraints. It determines a
* connected components in the auxiliary graph, where only tight (-, +) and (+, -) cross-tree edges
* are present. For these connected components it computes the same dual change, therefore the
* constraints imposed by the (-, +) and (+, -) cross-tree edges can't be violated. If a tight (+,
* +) cross-tree edge is encountered during this operation, an immediate augmentation is performed
* afterwards.
*
*
* @param the graph vertex type
* @param the graph edge type
* @author Timofey Chudakov
* @see BlossomVPrimalUpdater
* @see KolmogorovWeightedPerfectMatching
*/
class BlossomVDualUpdater
{
/**
* State information needed for the algorithm
*/
private BlossomVState state;
/**
* Instance of {@link BlossomVPrimalUpdater} for performing immediate augmentations after dual
* updates when they are applicable. These speed up the overall algorithm.
*/
private BlossomVPrimalUpdater primalUpdater;
/**
* Creates a new instance of the BlossomVDualUpdater
*
* @param state the state common to {@link BlossomVPrimalUpdater}, {@link BlossomVDualUpdater}
* and {@link KolmogorovWeightedPerfectMatching}
* @param primalUpdater primal updater used by the algorithm
*/
public BlossomVDualUpdater(BlossomVState state, BlossomVPrimalUpdater primalUpdater)
{
this.state = state;
this.primalUpdater = primalUpdater;
}
/**
* Performs global dual update. Operates on the whole graph and updates duals according to the
* strategy defined by {@link BlossomVOptions#dualUpdateStrategy}.
*
* @param type the strategy to use for updating the duals
* @return the sum of all changes of dual variables of the trees
*/
public double updateDuals(BlossomVOptions.DualUpdateStrategy type)
{
long start = System.nanoTime();
BlossomVEdge augmentEdge = null;
if (KolmogorovWeightedPerfectMatching.DEBUG) {
System.out.println("Start updating duals");
}
// go through all tree roots and determine the initial tree dual change wrt. in-tree
// constraints
// the cross-tree constraints are handles wrt. dual update strategy
for (BlossomVNode root = state.nodes[state.nodeNum].treeSiblingNext; root != null;
root = root.treeSiblingNext)
{
BlossomVTree tree = root.tree;
double eps = getEps(tree);
tree.accumulatedEps = eps - tree.eps;
}
if (type == MULTIPLE_TREE_FIXED_DELTA) {
augmentEdge = multipleTreeFixedDelta();
} else if (type == MULTIPLE_TREE_CONNECTED_COMPONENTS) {
augmentEdge = updateDualsConnectedComponents();
}
double dualChange = 0;
// add tree.accumulatedEps to the tree.eps
for (BlossomVNode root = state.nodes[state.nodeNum].treeSiblingNext; root != null;
root = root.treeSiblingNext)
{
if (root.tree.accumulatedEps > EPS) {
dualChange += root.tree.accumulatedEps;
root.tree.eps += root.tree.accumulatedEps;
}
}
if (KolmogorovWeightedPerfectMatching.DEBUG) {
for (BlossomVNode root = state.nodes[state.nodeNum].treeSiblingNext; root != null;
root = root.treeSiblingNext)
{
System.out
.println("Updating duals: now eps of " + root.tree + " is " + (root.tree.eps));
}
}
state.statistics.dualUpdatesTime += System.nanoTime() - start;
if (augmentEdge != null) {
primalUpdater.augment(augmentEdge);
}
return dualChange;
}
/**
* Computes and returns the value which can be assigned to the {@code tree.eps} so that it
* doesn't violate in-tree constraints. In other words, {@code getEps(tree) - tree.eps} is the
* resulting dual change wrt. in-tree constraints. The computed value is always greater than or
* equal to the {@code tree.eps}, can violate the cross-tree constraints, and can be equal to
* {@link KolmogorovWeightedPerfectMatching#INFINITY}.
*
* @param tree the tree to process
* @return a value which can be safely assigned to tree.eps
*/
private double getEps(BlossomVTree tree)
{
double eps = KolmogorovWeightedPerfectMatching.INFINITY;
// check minimum slack of the plus-infinity edges
if (!tree.plusInfinityEdges.isEmpty()) {
BlossomVEdge edge = tree.plusInfinityEdges.findMin().getValue();
if (edge.slack < eps) {
eps = edge.slack;
}
}
// check minimum dual variable of the "-" blossoms
if (!tree.minusBlossoms.isEmpty()) {
BlossomVNode node = tree.minusBlossoms.findMin().getValue();
if (node.dual < eps) {
eps = node.dual;
}
}
// check minimum slack of the (+, +) edges
if (!tree.plusPlusEdges.isEmpty()) {
BlossomVEdge edge = tree.plusPlusEdges.findMin().getValue();
if (2 * eps > edge.slack) {
eps = edge.slack / 2;
}
}
return eps;
}
/**
* Updates the duals of the single tree. This method takes into account both in-tree and
* cross-tree constraints. If possible, it also finds a cross-tree (+, +) edge of minimum slack
* and performs an augmentation.
*
* @param tree the tree to update duals of
* @return true iff some progress was made and there was no augmentation performed, false
* otherwise
*/
public boolean updateDualsSingle(BlossomVTree tree)
{
long start = System.nanoTime();
double eps = getEps(tree); // include only constraints on (+,+) in-tree edges, (+, inf)
// edges and "-' blossoms
double epsAugment = KolmogorovWeightedPerfectMatching.INFINITY; // takes into account
// constraints of the
// cross-tree edges
BlossomVEdge augmentEdge = null; // the (+, +) cross-tree edge of minimum slack
double delta = 0;
for (BlossomVTree.TreeEdgeIterator iterator = tree.treeEdgeIterator();
iterator.hasNext();)
{
BlossomVTreeEdge treeEdge = iterator.next();
BlossomVTree opposite = treeEdge.head[iterator.getCurrentDirection()];
if (!treeEdge.plusPlusEdges.isEmpty()) {
BlossomVEdge plusPlusEdge = treeEdge.plusPlusEdges.findMin().getValue();
if (plusPlusEdge.slack - opposite.eps < epsAugment) {
epsAugment = plusPlusEdge.slack - opposite.eps;
augmentEdge = plusPlusEdge;
}
}
MergeableAddressableHeap currentPlusMinusHeap =
treeEdge.getCurrentPlusMinusHeap(opposite.currentDirection);
if (!currentPlusMinusHeap.isEmpty()) {
BlossomVEdge edge = currentPlusMinusHeap.findMin().getValue();
if (edge.slack + opposite.eps < eps) {
eps = edge.slack + opposite.eps;
}
}
}
if (eps > epsAugment) {
eps = epsAugment;
}
// now eps takes into account all the constraints
if (eps > KolmogorovWeightedPerfectMatching.NO_PERFECT_MATCHING_THRESHOLD) {
throw new IllegalArgumentException(
KolmogorovWeightedPerfectMatching.NO_PERFECT_MATCHING);
}
if (eps > tree.eps) {
delta = eps - tree.eps;
tree.eps = eps;
if (KolmogorovWeightedPerfectMatching.DEBUG) {
System.out.println("Updating duals: now eps of " + tree + " is " + eps);
}
}
state.statistics.dualUpdatesTime += System.nanoTime() - start;
if (augmentEdge != null && epsAugment <= tree.eps) {
primalUpdater.augment(augmentEdge);
return false; // can't proceed with the same tree
} else {
return delta > EPS;
}
}
/**
* Updates the duals via connected components. The connected components are a set of trees which
* are connected via tight (+, -) cross tree edges. For these components the same dual change is
* chosen. As a result, the circular constraints are guaranteed to be avoided. This is the point
* where the {@link BlossomVDualUpdater#updateDualsSingle} approach can fail.
*/
private BlossomVEdge updateDualsConnectedComponents()
{
BlossomVTree dummyTree = new BlossomVTree();
BlossomVEdge augmentEdge = null;
double augmentEps = INFINITY;
double oppositeEps;
for (BlossomVNode root = state.nodes[state.nodeNum].treeSiblingNext; root != null;
root = root.treeSiblingNext)
{
root.tree.nextTree = null;
}
for (BlossomVNode root = state.nodes[state.nodeNum].treeSiblingNext; root != null;
root = root.treeSiblingNext)
{
BlossomVTree startTree = root.tree;
if (startTree.nextTree != null) {
// this tree is present in some connected component and has been processed already
continue;
}
double eps = startTree.accumulatedEps;
startTree.nextTree = startTree;
BlossomVTree connectedComponentLast = startTree;
BlossomVTree currentTree = startTree;
while (true) {
for (BlossomVTree.TreeEdgeIterator iterator = currentTree.treeEdgeIterator();
iterator.hasNext();)
{
BlossomVTreeEdge currentEdge = iterator.next();
int dir = iterator.getCurrentDirection();
BlossomVTree opposite = currentEdge.head[dir];
double plusPlusEps = KolmogorovWeightedPerfectMatching.INFINITY;
int dirRev = 1 - dir;
if (!currentEdge.plusPlusEdges.isEmpty()) {
plusPlusEps = currentEdge.plusPlusEdges.findMin().getKey() - currentTree.eps
- opposite.eps;
if (augmentEps > plusPlusEps) {
augmentEps = plusPlusEps;
augmentEdge = currentEdge.plusPlusEdges.findMin().getValue();
}
}
if (opposite.nextTree != null && opposite.nextTree != dummyTree) {
// opposite tree is in the same connected component
// since the trees in the same connected component have the same dual change
// we don't have to check (-, +) edges in this tree edge
if (2 * eps > plusPlusEps) {
eps = plusPlusEps / 2;
}
continue;
}
double[] plusMinusEps = new double[2];
plusMinusEps[dir] = KolmogorovWeightedPerfectMatching.INFINITY;
if (!currentEdge.getCurrentPlusMinusHeap(dir).isEmpty()) {
plusMinusEps[dir] =
currentEdge.getCurrentPlusMinusHeap(dir).findMin().getKey()
- currentTree.eps + opposite.eps;
}
plusMinusEps[dirRev] = KolmogorovWeightedPerfectMatching.INFINITY;
if (!currentEdge.getCurrentPlusMinusHeap(dirRev).isEmpty()) {
plusMinusEps[dirRev] =
currentEdge.getCurrentPlusMinusHeap(dirRev).findMin().getKey()
- opposite.eps + currentTree.eps;
}
if (opposite.nextTree == dummyTree) {
// opposite tree is in another connected component and has valid accumulated
// eps
oppositeEps = opposite.accumulatedEps;
} else if (plusMinusEps[0] > 0 && plusMinusEps[1] > 0) {
// this tree edge doesn't contain any tight (-, +) cross-tree edge and
// opposite tree
// hasn't been processed yet.
oppositeEps = 0;
} else {
// opposite hasn't been processed and there is a tight (-, +) cross-tree
// edge between
// current tree and opposite tree => we add opposite to the current
// connected component
connectedComponentLast.nextTree = opposite;
connectedComponentLast = opposite.nextTree = opposite;
if (eps > opposite.accumulatedEps) {
// eps of the connected component can't be greater than the minimum
// accumulated eps among trees in the connected component
eps = opposite.accumulatedEps;
}
continue;
}
if (eps > plusPlusEps - oppositeEps) {
// eps is bounded by the resulting slack of a (+, +) cross-tree edge
eps = plusPlusEps - oppositeEps;
}
if (eps > plusMinusEps[dir] + oppositeEps) {
// eps is bounded by the resulting slack of a (+, -) cross-tree edge in the
// current direction
eps = plusMinusEps[dir] + oppositeEps;
}
}
if (currentTree.nextTree == currentTree) {
// the end of the connected component
break;
}
currentTree = currentTree.nextTree;
}
if (eps > KolmogorovWeightedPerfectMatching.NO_PERFECT_MATCHING_THRESHOLD) {
throw new IllegalArgumentException(
KolmogorovWeightedPerfectMatching.NO_PERFECT_MATCHING);
}
// apply dual change to all trees in the connected component
BlossomVTree nextTree = startTree;
do {
currentTree = nextTree;
nextTree = nextTree.nextTree;
currentTree.nextTree = dummyTree;
currentTree.accumulatedEps = eps;
} while (currentTree != nextTree);
}
if (augmentEdge != null && augmentEps - augmentEdge.head[0].tree.accumulatedEps
- augmentEdge.head[1].tree.accumulatedEps <= 0)
{
return augmentEdge;
}
return null;
}
/**
* Updates duals by iterating through trees and greedily increasing their dual variables.
*/
private BlossomVEdge multipleTreeFixedDelta()
{
if (KolmogorovWeightedPerfectMatching.DEBUG) {
System.out.println("Multiple tree fixed delta approach");
}
BlossomVEdge augmentEdge = null;
double eps = INFINITY;
double augmentEps = INFINITY;
for (BlossomVNode root = state.nodes[state.nodeNum].treeSiblingNext; root != null;
root = root.treeSiblingNext)
{
BlossomVTree tree = root.tree;
double treeEps = tree.eps;
eps = Math.min(eps, tree.accumulatedEps);
// iterate only through outgoing tree edges so that every edge is considered only once
for (BlossomVTreeEdge outgoingTreeEdge = tree.first[0]; outgoingTreeEdge != null;
outgoingTreeEdge = outgoingTreeEdge.next[0])
{
// since all epsilons are equal we don't have to check (+, -) cross tree edges
if (!outgoingTreeEdge.plusPlusEdges.isEmpty()) {
BlossomVEdge varEdge = outgoingTreeEdge.plusPlusEdges.findMin().getValue();
double slack = varEdge.slack - treeEps - outgoingTreeEdge.head[0].eps;
eps = Math.min(eps, slack / 2);
if (augmentEps > slack) {
augmentEps = slack;
augmentEdge = varEdge;
}
}
}
}
if (eps > KolmogorovWeightedPerfectMatching.NO_PERFECT_MATCHING_THRESHOLD) {
throw new IllegalArgumentException(
KolmogorovWeightedPerfectMatching.NO_PERFECT_MATCHING);
}
for (BlossomVNode root = state.nodes[state.nodeNum].treeSiblingNext; root != null;
root = root.treeSiblingNext)
{
root.tree.accumulatedEps = eps;
}
if (augmentEps <= 2 * eps) {
return augmentEdge;
}
return null;
}
}