Files
BiGpairSEQ/src/main/java/MaximumIntegerWeightBipartiteMatching.java

1285 lines
56 KiB
Java

import org.jgrapht.Graph;
import org.jgrapht.GraphTests;
import org.jgrapht.GraphType;
import org.jgrapht.Graphs;
import org.jgrapht.alg.connectivity.GabowStrongConnectivityInspector;
import org.jgrapht.alg.interfaces.MatchingAlgorithm;
import java.io.Serializable;
import java.math.BigInteger;
import java.math.BigDecimal;
import java.math.RoundingMode;
import org.jgrapht.alg.shortestpath.DijkstraShortestPath;
import org.jgrapht.graph.*;
import org.jgrapht.graph.builder.GraphBuilder;
import org.jheaps.*;
import org.jheaps.tree.*;
import java.util.*;
import java.util.function.*;
import java.util.stream.Collectors;
/**
* Maximum weight matching in bipartite graphs with strictly integer edge weights, using a scaling algorithm
* via Duan and Su with O(m * sqrt(n) * log(N)) running time, where m is the number of edges, n is the number
* of vertices in the larger partition of the graph, and N is the maximum integer edge weight.
*
* Behavior when given a graph without strictly integer weights is undefined.
*
* See:
* "A Scaling Algorithm for Maximum Weight Matching in Bipartite Graphs"
* Ran Duan and Hsin-Hao Su, Proceedings of the Twenty-Third Annual ACM-SIAM Symposium on Discrete Algorithms, p. 1413-1424. (2012)
* https://web.eecs.umich.edu/~pettie/matching/Duan-Su-scaling-bipartite-matching.pdf
*
* @param <V> the graph vertex type
* @param <E> the graph edge type
*
* @author Eugene Fischer
*/
public class MaximumIntegerWeightBipartiteMatching<V, E> implements MatchingAlgorithm<V, E> {
private final Graph<V, E> graph;
private final Set<V> partition1;
private final Set<V> partition2;
private final Set<E> matching;
private final BigInteger minPartitionSize;
private final BigDecimal maxEdgeWeight; //must be an integer value
private BigInteger matchingWeight = BigInteger.ZERO;
//the number three as a BigDecimal
private final BigDecimal THREE = new BigDecimal(3);
// the initial potential constant
private final BigDecimal delta_0;
// the potential constant used in a given scaling phase
private BigDecimal delta;
// the number of scaling steps to use
private final Integer numberOfScales;
// the current scaling step
private Integer currentScale = 0;
// vertex potentials, not necessarily integers
private Map<V, BigDecimal> potentials = new HashMap<>();
;
// the matched edge of a vertex, also used to check if a vertex is free
private Map<V, E> matchedEdge;
private final Comparator<BigDecimal> decimalComparator;
private final Comparator<BigInteger> integerComparator;
private final Function<Comparator<BigDecimal>, AddressableHeap<BigDecimal, V>> heapSupplier;
// data structures for finding alternating paths
private AddressableHeap<BigDecimal, V> heap;
private Map<V, AddressableHeap.Handle<BigDecimal, V>> nodeInHeap;
private Map<V, E> predecessor;
private Map<V, BigDecimal> distance;
/*
* Constructor without a given maximum edge weight or addressable heap supplier
*
* @param graph the input graph
* @param partition1 the first partition of the vertex set
* @param partition2 the second partition of the vertex set
* @throws IllegalArgumentException if graph is not undirected
* @throws NumberFormatException if there are non-integer edge weights
*/
public MaximumIntegerWeightBipartiteMatching(Graph<V, E> graph, Set<V> partition1, Set<V> partition2) {
this(graph, partition1, partition2, new BigDecimal(String.valueOf(graph
.edgeSet()
.stream()
.mapToDouble(graph::getEdgeWeight)
// .peek( w -> new BigInteger(String.valueOf(w)))
.max())),
FibonacciHeap::new);
}
/*
* Constructor without a given maximum edge weight
*
* @param graph the input graph
* @param partition1 the first partition of the vertex set
* @param partition2 the second partition of the vertex set
* @param heapSupplier a supplier for the addressable heap to use in the algorithm
* @throws IllegalArgumentException if graph is not undirected
* @throws NumberFormatException if there are non-integer edge weights
*/
public MaximumIntegerWeightBipartiteMatching(Graph<V, E> graph, Set<V> partition1, Set<V> partition2,
Function<Comparator<BigDecimal>, AddressableHeap<BigDecimal, V>> heapSupplier) {
this(graph, partition1, partition2, new BigDecimal(String.valueOf(graph
.edgeSet()
.stream()
.mapToDouble(graph::getEdgeWeight)
.peek(w -> new BigInteger(String.valueOf(w)))
.max())),
heapSupplier);
}
/*
* Constructor without a given addressable heap supplier
*
* @param graph is the input graph
* @param partition1 is the first partition of the vertex set
* @param partition2 is the second partition of the vertex set
* @param maxEdgeWeight the maximum edge weight present in the graph (which must be an integer value)
* @throws IllegalArgumentException if graph is not undirected
* @throws NumberFormatException if there are non-integer edge weights
*/
public MaximumIntegerWeightBipartiteMatching(Graph<V, E> graph, Set<V> partition1, Set<V> partition2,
BigDecimal maxEdgeWeight) {
this(graph, partition1, partition2, maxEdgeWeight, FibonacciHeap::new);
}
/*
* Constructor.
*
* Does not check to confirm that all edge weights are integer values. Behavior for a graph with
* non-integer weights is undefined.
*
* @param graph is the input graph
* @param partition1 is the first partition of the vertex set
* @param partition2 is the second partition of the vertex set
* @param maxEdgeWeight the maximum edge weight present in the graph (which must be an integer value)
* @param heapSupplier a supplier for the addressable heap to use in the algorithm
* @throws IllegalArgumentException if graph is not undirected
* @throws IllegalArgumentException maxEdgeWeight is not an integer value
*/
public MaximumIntegerWeightBipartiteMatching(Graph<V, E> graph, Set<V> partition1, Set<V> partition2,
BigDecimal maxEdgeWeight,
Function<Comparator<BigDecimal>, AddressableHeap<BigDecimal, V>> heapSupplier) {
this.graph = GraphTests.requireUndirected(graph);
if (!GraphTests.isBipartitePartition(graph, partition1, partition2)) {
throw new IllegalArgumentException("Graph partition is not bipartite");
}
try {
int test = maxEdgeWeight.intValueExact();
} catch (ArithmeticException error) {
throw new IllegalArgumentException("Maximum edge weight must be an integer value small enough to fit in an int");
}
// Ensure that partition1 is smaller or equal in size compared to partition 2
if (partition1.size() <= partition2.size()) {
this.partition1 = Objects.requireNonNull(partition1, "Partition 1 cannot be null");
this.partition2 = Objects.requireNonNull(partition2, "Partition 2 cannot be null");
} else { // else, swap
this.partition1 = Objects.requireNonNull(partition2, "Partition 2 cannot be null");
this.partition2 = Objects.requireNonNull(partition1, "Partition 1 cannot be null");
}
this.decimalComparator = Comparator.<BigDecimal>naturalOrder();
this.integerComparator = Comparator.<BigInteger>naturalOrder();
this.heapSupplier = Objects.requireNonNull(heapSupplier, "Heap supplier cannot be null");
Integer n = Math.min(partition1.size(), partition2.size());
this.minPartitionSize = new BigInteger(String.valueOf(Math.max(partition1.size(), partition2.size())));
this.maxEdgeWeight = maxEdgeWeight;
this.matching = new LinkedHashSet<>();
this.matchedEdge = new HashMap<>();
Double exponent = maxEdgeWeight.doubleValue() / Math.sqrt(Double.parseDouble(n.toString()));
exponent = Math.log(exponent) / Math.log(2); //log base two. log_b(n) = log(n) / log(b)
exponent = Math.floor(exponent);
// the initial potential constant
this.delta_0 = new BigDecimal(BigInteger.TWO.pow(Double.valueOf(exponent.toString()).intValue()));
this.delta = this.delta_0;
Double numberOfScales = Math.log(Double.parseDouble(maxEdgeWeight.toString())) / Math.log(2);
numberOfScales = Math.ceil(numberOfScales);
this.numberOfScales = Double.valueOf(numberOfScales.toString()).intValue();
}
public Map<V, BigDecimal> getPotentials() {
if (potentials == null) {
return Collections.emptyMap();
} else {
return Collections.unmodifiableMap(potentials);
}
}
/**
* {@inheritDoc}
*/
@Override
public Matching<V, E> getMatching() {
/*
* Test input instance
*/
if (!GraphTests.isSimple(graph)) {
throw new IllegalArgumentException("Only simple graphs supported");
}
if (!GraphTests.isBipartitePartition(graph, partition1, partition2)) {
throw new IllegalArgumentException("Graph partition is not bipartite");
}
// empty graph
if (graph.edgeSet().isEmpty()) {
return new MatchingImpl<>(graph, matching, matchingWeight.doubleValue());
}
/*
* ALGORITHM PHASE 1
* operates on scale 0
* in this phase an augmenting path must have free endpoints
*/
// initialize vertex potentials
for (V v : partition1) {
potentials.put(v, delta.multiply(bigDecimalFloor(maxEdgeWeight.divide(delta))));
}
for (V v : partition2) {
potentials.put(v, BigDecimal.ZERO);
}
while (numberOfFreeVertices(partition1) != 0 && !isZeroPotentialForFreeVertices(partition1)) {
// AUGMENTATION and DUAL ADJUSTMENT
phase1AugmentationAndDualAdjustment(BigDecimal.ONE, BigDecimal.ONE);
}
incrementScale();
/*
* ALGORITHM PHASE II
* operates on scale 1 - numberOfScales
* in this phase an augmenting path may start or end with matched edges
*/
while (currentScale <= numberOfScales) {
//update potentials for left vertices
partition1.forEach(v -> potentials.put(v, potentials.get(v).add(delta)));
/*
* Run one iteration of the Phase I Augmentation and Dual Adjustment procedures on G[1, 3].
* This is to correct the free-vertex duals condition.
*/
phase1AugmentationAndDualAdjustment(BigDecimal.ONE, THREE);
//AUGMENTATION
// stage 1 - eliminate augmenting cycles
// find a maximal set of vertex-disjoint augmenting cycles via modified DFS
Set<V> matchedVertices = matchedEdge.keySet();
Set<V> visitedVertices = new HashSet<>();
List<List<E>> cycles = new ArrayList<>();
for (V vertex : matchedVertices) {
//skip any vertices that have been visited already
if (visitedVertices.contains(vertex)) {
continue;
}
Stack<V> stack = new Stack<>();
Map<V, E> predecessor = new HashMap<V, E>();
stack.push(vertex);
while (!stack.empty()) {
V v = stack.pop();
if (!matchedVertices.contains(v)) {
//if this is a free vertex, ignore it
continue;
}
visitedVertices.add(v);
E edge = matchedEdge.get(v);
V v_mate = Graphs.getOppositeVertex(graph, edge, v);
visitedVertices.add(v_mate);
predecessor.put(v_mate, edge);
for (E e : graph.edgesOf(v_mate)) {
//ignore matched edges
if (matching.contains(e)) {
continue;
}
V nextVertex = Graphs.getOppositeVertex(graph, e, v_mate);
if (!visitedVertices.contains(nextVertex)) {
//if this vertex wasn't previously visited, add it to the stack
stack.push(nextVertex);
} else {
//check if nextVertex is an ancestor of v
V testVertex = v;
List<E> cycle = new ArrayList<>();
cycle.add(0, e);
cycle.add(0, edge);
while (predecessor.containsKey(testVertex)) {
E nextEdge = predecessor.get(testVertex);
cycle.add(0, nextEdge);
testVertex = Graphs.getOppositeVertex(graph, nextEdge, testVertex);
if (testVertex.equals(nextVertex)) {
//this is a cycle, add it
cycles.add(cycle);
//back up the search to before this cycle
V parent;
if (predecessor.containsKey(nextVertex)) {
parent = Graphs.getOppositeVertex(graph, predecessor.get(nextVertex), nextVertex);
} else {
parent = v;
}
while (!stack.empty() && !stack.peek().equals(parent)) {
stack.pop();
}
}
}
}
}
}
}
augmentMatching(cycles);
// stage 2 - eliminate augmenting paths
//starting vertices are zero potential vertices that are free if in partition1 or matched if in partition2
Set<V> startingVertices = partition1.stream()
.filter(v -> !matchedEdge.containsKey(v))
.filter(v -> potentials.get(v).compareTo(BigDecimal.ZERO) == 0)
.collect(Collectors.toCollection(HashSet::new));
startingVertices.addAll(partition2.stream()
.filter(v -> matchedEdge.containsKey(v))
.filter(v -> potentials.get(v).compareTo(BigDecimal.ZERO) == 0)
.collect(Collectors.toCollection(HashSet::new)));
List<List<E>> maximalAugmentingPaths = new ArrayList<>();
System.out.println("Starting vertex count: " + startingVertices.size());
visitedVertices = new HashSet<>();
Graph<V, E> directedGraph = new asDirectedGraph<V, E>(graph);
Graph<V, E> subgraphG13 = new MaskSubgraph<>(directedGraph, v -> true, e -> isEligibleEdge(e, BigDecimal.ONE, THREE));
for (V vertex : startingVertices) {
if (visitedVertices.contains(vertex)) {
continue;
}
Stack<V> stack = new Stack<>();
Map<V, E> predecessor = new HashMap<V, E>();
if (!matchedEdge.containsKey(vertex)) {
stack.push(vertex);
} else {
stack.push(Graphs.getOppositeVertex(graph, matchedEdge.get(vertex), vertex));
}
boolean stillSearching = true;
while (!stack.empty() && stillSearching) {
V v = stack.pop();
if(!subgraphG13.vertexSet().contains(v)){
continue;
}
visitedVertices.add(v);
for (E e : subgraphG13.edgesOf(v)) {
if (matching.contains(e)) {
continue;
}
V nextVertex = Graphs.getOppositeVertex(graph, e, v);
predecessor.put(nextVertex, e);
if (!matchedEdge.containsKey(nextVertex)) {
List<E> path = new ArrayList<>();
V testVertex = nextVertex;
while (predecessor.containsKey(testVertex)) {
E edge = predecessor.get(testVertex);
path.add(0, edge);
testVertex = Graphs.getOppositeVertex(graph, edge, testVertex);
}
maximalAugmentingPaths.add(path);
stillSearching = false;
} else {
E edge = matchedEdge.get(nextVertex);
stack.push(Graphs.getOppositeVertex(graph, edge, nextVertex));
}
}
if (stillSearching && potentials.get(v).compareTo(BigDecimal.ZERO) == 0) {
List<E> path = new ArrayList<>();
V testVertex = v;
while (predecessor.containsKey(testVertex)) {
E edge = predecessor.get(testVertex);
path.add(0, edge);
testVertex = Graphs.getOppositeVertex(graph, edge, testVertex);
}
maximalAugmentingPaths.add(path);
stillSearching = false;
}
}
}
augmentMatching(maximalAugmentingPaths);
//DUAL ADJUSTMENT
// --ANTICHAIN CASE
//get the set of violated edges
Set<E> violatedEdges = phase2GetViolatedEdges();
//get the set of adjustable vertices
//first find an intermediate set of vertices
Set<V> intermediateVertices = new HashSet<>();
for (V v : graph.vertexSet()) {
if (!matchedEdge.containsKey(v)) {
//add all free vertices
intermediateVertices.add(v);
} else if (potentials.get(Graphs.getOppositeVertex(graph, matchedEdge.get(v), v))
.compareTo(BigDecimal.ZERO) == 0) {
//add all matched vertices with a zero-potential mate
intermediateVertices.add(v);
}
}
//get V_odd(intermediateVertices, G[1, 3])
Set<V> nonAdjustableVertices = getEvenOddVertices(intermediateVertices, BigDecimal.ONE, THREE).get(1);
//finally, get the set of adjustable vertices
Set<V> adjustableVertices = graph.vertexSet()
.stream()
.filter(v -> !nonAdjustableVertices.contains(v))
.collect(Collectors.toCollection(HashSet::new));
//construct vertex sets X_L and X_R
Set<V> intermediateAdjustableSubsetLeft = adjustableVertices.stream()
.filter(partition1::contains)
.collect(Collectors.toCollection(HashSet::new));
Set<V> adjustableSubsetLeft = new HashSet<>();
for (V v : intermediateAdjustableSubsetLeft) {
if (graph.edgesOf(v).stream().anyMatch(violatedEdges::contains)) {
adjustableSubsetLeft.add(v);
}
}
Set<V> intermediateAdjustableSubsetRight = adjustableVertices.stream()
.filter(partition2::contains)
.collect(Collectors.toCollection(HashSet::new));
Set<V> adjustableSubsetRight = new HashSet<>();
for (V v : intermediateAdjustableSubsetRight) {
if (graph.edgesOf(v).stream().anyMatch(violatedEdges::contains)) {
adjustableSubsetRight.add(v);
}
}
Set<V> adjustableSubset = adjustableSubsetRight.size() > adjustableSubsetLeft.size() ?
adjustableSubsetRight :
adjustableSubsetLeft;
List<Set<V>> evenOddVertices = getEvenOddVertices(adjustableSubset, BigDecimal.ONE, THREE);
for (V v : evenOddVertices.get(0)) {
potentials.put(v, potentials.get(v).subtract(delta));
}
for (V v : evenOddVertices.get(1)) {
potentials.put(v, potentials.get(v).add(delta));
}
// --CHAIN CASE
//get a set of all the free vertices, these will be the "roots" of Dial's algorithm
Set<V> freeVertices = graph.vertexSet()
.stream()
.filter(v -> !matchedEdge.containsKey(v))
.collect(Collectors.toCollection(HashSet::new));
//create a set of buckets for each possible path distance. This can be a fixed array
//with a length of the number of vertices-1 times the maximum edge weight.
int numberOfBuckets;
//TODO: remove this
int maxIndex = 0; //this is just to test something, remove later
// try {
// numberOfBuckets = (graph.vertexSet().size() - 1) * maxEdgeWeight.intValueExact();
// } catch (ArithmeticException error) {
// //if the weights are too big for the above product to fit in an int,
// numberOfBuckets = Integer.MAX_VALUE - 1;
// }
numberOfBuckets = bigDecimalCeil(delta).intValueExact();
List<List<E>> shortestPaths = new ArrayList<>();
for (V v : freeVertices) {
//This needs to be a list of lists, because there can be more than one vertex with the same distance
List<List<V>> buckets = new ArrayList<>(numberOfBuckets);
while (buckets.size() < numberOfBuckets) {
buckets.add(new ArrayList<>());
}
// Collections.fill(buckets, new ArrayList<V>());
Map<V, V> previousVertex = new HashMap<>();
Map<V, BigDecimal> distances = new HashMap<>();
buckets.get(0).add(v);
distances.put(v, BigDecimal.ZERO);
System.out.println("Actual number of buckets: " + buckets.size());
for (int i = 0; i < buckets.size(); i++) {
List<V> vertices = buckets.get(i);
if (vertices.size() == 0) {
continue;
}
//TODO: remove this
maxIndex = i;
i--; //reduce i because we need to check this bucket again next iteration.
V vertex = vertices.remove(0);
Set<E> outgoingEdges = phase2GetOutgoingEdges(v, vertex);
for (E e : outgoingEdges) {
if (!isEligibleEdge(e, BigDecimal.ZERO, THREE)) {
continue;
}
BigDecimal edgeWeight = phase2GetEdgeWeight(e);
V nextVertex = Graphs.getOppositeVertex(graph, e, vertex);
BigDecimal distance = distances.get(vertex).add(edgeWeight);
if (!distances.containsKey(nextVertex)) {
distances.put(nextVertex, distance);
buckets.get(distance.intValue()).add(nextVertex);
previousVertex.put(nextVertex, vertex);
} else {
BigDecimal prevDistance = distances.get(nextVertex);
if (distance.compareTo(prevDistance) < 0) {
distances.put(nextVertex, distance);
buckets.get(prevDistance.intValue()).remove(nextVertex);
buckets.get(distance.intValue()).add(nextVertex);
previousVertex.put(nextVertex, vertex);
}
}
}
}
if(maxIndex == 0) {
continue;
}
//check which side of the graph v is on
boolean vInPartition1 = partition1.contains(v);
BigDecimal dualAdjustAmount = new BigDecimal(numberOfBuckets + 1);
V endVertex = v;
for (Map.Entry<V, BigDecimal> entry : distances.entrySet()) {
BigDecimal newAmount;
if (entry.getKey().equals(v)) {
continue;
}
if (partition1.contains(entry.getKey()) != vInPartition1 && !matchedEdge.containsKey(entry.getKey())) {
newAmount = entry.getValue();
} else if (partition1.contains(entry.getKey()) == vInPartition1) {
newAmount = entry.getValue().add(potentials.get(entry.getKey()));
} else {
newAmount = new BigDecimal(numberOfBuckets + 1);
}
if (newAmount.compareTo(dualAdjustAmount) < 0) {
dualAdjustAmount = newAmount;
endVertex = entry.getKey();
}
}
dualAdjustAmount = dualAdjustAmount.subtract(distances.get(endVertex));
if (dualAdjustAmount.compareTo(BigDecimal.ZERO) < 1) {
dualAdjustAmount = BigDecimal.ZERO;
}
if (partition1.contains(endVertex) == vInPartition1) {
potentials.put(endVertex, potentials.get(endVertex).subtract(dualAdjustAmount));
} else {
potentials.put(endVertex, potentials.get(endVertex).add(dualAdjustAmount));
}
List<E> path = new ArrayList<>();
V testVertex = endVertex;
while (previousVertex.containsKey(testVertex)) {
E edge = graph.getEdge(testVertex, previousVertex.get(testVertex));
path.add(0, edge);
testVertex = previousVertex.get(testVertex);
}
shortestPaths.add(path);
}
augmentMatching(shortestPaths);
//TODO: remove this
System.out.println("max distance reached: " + maxIndex);
if (currentScale == numberOfScales) {
break;
}
incrementScale();
}
/*
* ALGORITHM PHASE III
* operates on scale numberOfScales
*/
//detect cycles
Graph<V, E> directedGraph = new asDirectedGraph<V, E>(graph);
Graph<V, E> subgraphG01 = new MaskSubgraph<>(directedGraph, v -> true, e -> isEligibleEdge(e, BigDecimal.ZERO, BigDecimal.ONE));
GabowStrongConnectivityInspector<V, E> sscInspectorCycles = new GabowStrongConnectivityInspector<V, E>(subgraphG01);
List<Set<V>> stronglyConnectedComponents01 = sscInspectorCycles.stronglyConnectedSets();
Set<E> nonTightEdges = new HashSet<>();
for (E e: subgraphG01.edgeSet()) {
if (matching.contains(e) && getEdgePotential(e) != getScaledWeight(e)) {
nonTightEdges.add(e);
}
}
Set<E> nonTightEdgesInCycles = new HashSet<>();
for (E e: nonTightEdges) {
V source = subgraphG01.getEdgeSource(e);
V target = subgraphG01.getEdgeTarget(e);
for (Set<V> vertices: stronglyConnectedComponents01) {
if (vertices.contains(source) && vertices.contains(target)) {
nonTightEdgesInCycles.add(e);
}
}
}
// find a maximal set of vertex-disjoint augmenting cycles via modified DFS
List<List<E>> cycles = new ArrayList<>();
for(E e: nonTightEdgesInCycles) {
List<E> edgeList = DijkstraShortestPath.findPathBetween(subgraphG01, subgraphG01.getEdgeTarget(e), subgraphG01.getEdgeSource(e)).getEdgeList();
edgeList.add(e);
cycles.add(edgeList);
}
augmentMatching(cycles);
//detect paths
Set<V> nonAdjustableVertices = graph.vertexSet()
.stream()
.filter(v-> (!matchedEdge.containsKey(v) ||
(matchedEdge.containsKey(v) && potentials.get(Graphs.getOppositeVertex(graph, matchedEdge.get(v), v) ).equals(BigDecimal.ZERO))))
.collect(Collectors.toSet());
Set<E> nonTightMatchedEdges = matching.stream()
.filter(e -> !getScaledWeight(e).equals(getEdgePotential(e)))
.collect(Collectors.toSet());
boolean augmentingPathsExist = false;
for (E e: nonTightEdges) {
if (nonAdjustableVertices.contains(graph.getEdgeSource(e)) && nonAdjustableVertices.contains(graph.getEdgeTarget(e))) {
augmentingPathsExist = true;
break;
}
}
if (augmentingPathsExist) {
//largely repeated code, abstract this
Set<V> startingVertices = partition1.stream()
.filter(v -> !matchedEdge.containsKey(v))
.filter(v -> potentials.get(v).compareTo(BigDecimal.ZERO) == 0)
.collect(Collectors.toCollection(HashSet::new));
startingVertices.addAll(partition2.stream()
.filter(v -> matchedEdge.containsKey(v))
.filter(v -> potentials.get(v).compareTo(BigDecimal.ZERO) == 0)
.collect(Collectors.toCollection(HashSet::new)));
List<List<E>> maximalAugmentingPaths = new ArrayList<>();
System.out.println("Starting vertex count: " + startingVertices.size());
Set <V> visitedVertices = new HashSet<>();
for (V vertex : startingVertices) {
if (visitedVertices.contains(vertex)) {
continue;
}
Stack<V> stack = new Stack<>();
Map<V, E> predecessor = new HashMap<V, E>();
if (!matchedEdge.containsKey(vertex)) {
stack.push(vertex);
} else {
stack.push(Graphs.getOppositeVertex(graph, matchedEdge.get(vertex), vertex));
}
boolean stillSearching = true;
while (!stack.empty() && stillSearching) {
V v = stack.pop();
if(!subgraphG01.vertexSet().contains(v)){
continue;
}
visitedVertices.add(v);
for (E e : subgraphG01.edgesOf(v)) {
if (matching.contains(e)) {
continue;
}
V nextVertex = Graphs.getOppositeVertex(graph, e, v);
predecessor.put(nextVertex, e);
if (!matchedEdge.containsKey(nextVertex)) {
List<E> path = new ArrayList<>();
V testVertex = nextVertex;
while (predecessor.containsKey(testVertex)) {
E edge = predecessor.get(testVertex);
path.add(0, edge);
testVertex = Graphs.getOppositeVertex(graph, edge, testVertex);
}
maximalAugmentingPaths.add(path);
stillSearching = false;
} else {
E edge = matchedEdge.get(nextVertex);
stack.push(Graphs.getOppositeVertex(graph, edge, nextVertex));
}
}
if (stillSearching && potentials.get(v).compareTo(BigDecimal.ZERO) == 0) {
List<E> path = new ArrayList<>();
V testVertex = v;
while (predecessor.containsKey(testVertex)) {
E edge = predecessor.get(testVertex);
path.add(0, edge);
testVertex = Graphs.getOppositeVertex(graph, edge, testVertex);
}
maximalAugmentingPaths.add(path);
stillSearching = false;
}
}
}
augmentMatching(maximalAugmentingPaths);
}
//dual adjustment
Graph<V, E> subgraphG00 = new MaskSubgraph<>(directedGraph, v -> true, e -> isEligibleEdge(e, BigDecimal.ZERO, BigDecimal.ZERO));
GabowStrongConnectivityInspector<V, E> sscInspectorPaths = new GabowStrongConnectivityInspector<V, E>(subgraphG00);
List<Set<V>> stronglyConnectedComponents00 = sscInspectorPaths.stronglyConnectedSets();
Map<Integer, V> dummyVertices = new HashMap<>();
for (int i = 0; i < stronglyConnectedComponents00.size(); i++) {
dummyVertices.put(i, stronglyConnectedComponents00.get(i).iterator().next());
}
Graph<V, DefaultEdge> condensedG01 = new SimpleDirectedGraph<V, DefaultEdge>(DefaultEdge.class);
for(V v: subgraphG01.vertexSet()) {
boolean condenseThis = false;
for (int i = 0; i < stronglyConnectedComponents00.size(); i++) {
if (stronglyConnectedComponents00.get(i).contains(v)) {
condenseThis = true;
condensedG01.addVertex(dummyVertices.get(i));
break;
}
}
if (!condenseThis) {
condensedG01.addVertex(v);
}
}
for (E e: subgraphG01.edgeSet()) {
boolean replaceThis = false;
V source = subgraphG01.getEdgeSource(e);
V target = subgraphG01.getEdgeTarget(e);
for (int i = 0; i < stronglyConnectedComponents00.size(); i++) {
if (stronglyConnectedComponents00.get(i).contains(source)
&& stronglyConnectedComponents00.get(i).contains(target)) {
replaceThis = true;
break;
}
else if (stronglyConnectedComponents00.get(i).contains(source)) {
replaceThis = true;
condensedG01.addEdge(dummyVertices.get(i), target);
break;
}
else if (stronglyConnectedComponents00.get(i).contains(target)) {
replaceThis = true;
condensedG01.addEdge(source, dummyVertices.get(i));
break;
}
}
if (!replaceThis) {
condensedG01.addEdge(source, target);
}
}
//longest path must start from a vertex with in degree of 0
Set<V> zeroInDegreeVertices = condensedG01.vertexSet().stream().filter(v -> condensedG01.inDegreeOf(v) == 0).collect(Collectors.toSet());
//TODO: finish this
//print bad edge count
long badEdges = matching.stream().filter(e -> getEdgePotential(e).subtract(getScaledWeight(e)).equals(delta)).count();
System.out.println("Bad edge count: "+ badEdges);
return new MatchingImpl<>(graph, this.matching, this.matching.stream().map(graph::getEdgeWeight).reduce(0.0, (a, b) -> a + b));
}
/*
* Returns the set of violated edges in the current matching. These are the edges for which the
* sum of the
*/
private Set<E> phase2GetViolatedEdges() {
Set<E> violatedEdges = new HashSet<E>();
for (E e : matching) {
BigDecimal edgePotential = getEdgePotential(e);
BigDecimal scaledWeight = getScaledWeight(e);
BigDecimal difference = edgePotential.subtract(scaledWeight);
if (difference.compareTo(delta) > 0) {
violatedEdges.add(e);
}
}
return violatedEdges;
}
/*
* Returns the weight for an edge as needed for the search function in Phase 2
*/
private BigDecimal phase2GetEdgeWeight(E e) {
if (matching.contains(e)) {
return BigDecimal.ZERO;
} else {
return getEdgePotential(e).subtract(getScaledWeight(e));
}
}
/*
* Return outgoing edges of the associated directed graph, as needed for the search
* function in Phase 2
*/
private Set<E> phase2GetOutgoingEdges(V startingVector, V v) {
if (partition2.contains(startingVector)) {
return getOutgoingEdges(v);
} else {
return getOutgoingEdgesTranspose(v);
}
}
/*
* The Augmentation and Dual Adjustment steps of Phase I of the algorithm, using eligibility
* subgraph G[c, d]. This is abstracted out because these steps are used again in Phase II with
* a different eligibility subgraph than in Phase I.
*/
private void phase1AugmentationAndDualAdjustment(BigDecimal c, BigDecimal d) {
// AUGMENTATION
// Find a maximal set of augmenting paths in G[c, d]
List<List<E>> augmentingPaths = new ArrayList<>();
Set<V> usedVertices = new HashSet<>();
//in phase 1, the endpoints of an augmenting path must be free vertices
for (V v : partition1.stream()
.filter(this::isFreeVertex)
.collect(Collectors.toCollection(HashSet::new))) {
augmentingPaths.add(getAugmentingPathPhase1(v, usedVertices, c, d));
}
augmentMatching(augmentingPaths);
//DUAL ADJUSTMENT
Set<V> leftFreeVertices = partition1.stream()
.filter(this::isFreeVertex)
.collect(Collectors.toCollection(HashSet::new));
List<Set<V>> evenOddVertices = getEvenOddVertices(leftFreeVertices, c, d);
Set<V> evenVertices = evenOddVertices.get(0);
Set<V> oddVertices = evenOddVertices.get(1);
for (V v : evenVertices) {
potentials.put(v, potentials.get(v).subtract(delta));
}
for (V v : oddVertices) {
potentials.put(v, potentials.get(v).add(delta));
}
}
/*
* In Phase 1 an augmenting path is an alternating path in G[c, d] with free endpoints.
* Function returns a maximal set of vertex-disjoint, shortest augmenting paths via breadth
* first search, as in Hopcroft-Karp
*/
private List<E> getAugmentingPathPhase1(V v, Set<V> usedVertices, BigDecimal c, BigDecimal d) {
List<E> path = new ArrayList<>();
Set<V> visited = new HashSet<>();
Deque<V> queue = new ArrayDeque<>();
Map<V, E> predecessor = new HashMap<V, E>();
if (!usedVertices.contains(v)) {
queue.add(v);
}
while (queue.size() > 0) {
V vertex = queue.removeFirst();
for (E e : graph.edgesOf(vertex)) {
if (predecessor.containsKey(vertex)) {
if (isMatchedEdge(e) == isMatchedEdge(predecessor.get(vertex))) { //ensure this is an alternating path
continue;
}
}
if (!isEligibleEdge(e, c, d)) { //ensure this edge is in G[1, 1]
continue;
}
V nextVertex = Graphs.getOppositeVertex(graph, e, vertex);
if (visited.contains(nextVertex)) { //we've already found a shorter path here
continue;
}
if (usedVertices.contains(nextVertex)) { //this vertex is already part of a path
continue;
}
visited.add(nextVertex);
predecessor.put(nextVertex, e);
if (isFreeVertex(nextVertex)) {
//this is the shortest augmenting path
while (predecessor.containsKey(nextVertex)) {
usedVertices.add(nextVertex);
E edge = predecessor.get(nextVertex);
path.add(0, edge);
nextVertex = Graphs.getOppositeVertex(graph, edge, nextVertex);
}
usedVertices.add(nextVertex);
return path;
} else {
queue.add(nextVertex);
}
}
}
return path;
}
/*
* Given a set of vertices, returns how many vertices in the set are not currently matched.
*/
private long numberOfFreeVertices(Set<V> vertices) {
return vertices.stream().filter(this::isFreeVertex).count();
}
/*
* Given a set of vertices, returns whether every unmatched vertex in the set
* has a potential value of 0.
*/
private boolean isZeroPotentialForFreeVertices(Set<V> vertices) {
for (V v : vertices) {
if (matchedEdge.containsKey(v)) {
continue;
}
if (potentials.get(v).compareTo(BigDecimal.ZERO) != 0) {
return false;
}
}
return true;
}
/*
* Given a set of augmenting paths, passed as a List of Lists of edges, augments the current
* matching along all included paths.
*/
private void augmentMatching(List<List<E>> augmentingPaths) {
for (List<E> path : augmentingPaths) {
for (E edge : path) {
V s = graph.getEdgeSource(edge);
V t = graph.getEdgeTarget(edge);
if (matching.contains(edge)) {
matchedEdge.remove(s);
matchedEdge.remove(t);
matching.remove(edge);
} else {
matchedEdge.put(s, edge);
matchedEdge.put(t, edge);
matching.add(edge);
}
}
}
System.out.println("matching size: " + matching.size());
}
private Graph<V, E> getEligibilitySubgraph(BigDecimal c, BigDecimal d) {
return new MaskSubgraph<>(graph, v -> true, e -> ((matching.contains(e)
&& getEdgePotential(e).equals(getScaledWeight(e))
|| (!matching.contains(e)
&& getScaledWeight(e).add(delta.multiply(c)).compareTo(getEdgePotential(e)) < 1
&& getEdgePotential(e).compareTo(getScaledWeight(e).add(delta.multiply(d))) < 1))));
}
private List<Set<V>> getEvenOddVertices(Set<V> startingVertices, Graph<V, E> graph) {
Set<V> visitedVertices = new HashSet<>();
Set<V> oddVertices = new HashSet<>();
Set<V> evenVertices = new HashSet<>();
for (V vertex : startingVertices) {
//depth first search
Stack<V> stack = new Stack<>();
Map<V, Integer> distance = new HashMap<>();
if (!visitedVertices.contains(vertex)) {
stack.push(vertex);
distance.put(vertex, 0);
}
while (!stack.isEmpty()) {
V v = stack.pop();
if (!visitedVertices.contains(v)) {
visitedVertices.add(v);
if (distance.get(v) % 2 == 0) {
// reachable via an even-length path
evenVertices.add(v);
} else {
// reachable via an odd-length path
oddVertices.add(v);
}
for (E edge : graph.edgesOf(v)) {
// if this is a vertex in the starting set, only consider unmatched edges
if (startingVertices.contains(v) && matching.contains(edge)) {
continue;
}
// only consider edges in G[c, d]
V nextVertex = Graphs.getOppositeVertex(graph, edge, v);
stack.push(nextVertex);
distance.put(nextVertex, distance.get(v) + 1);
}
}
}
}
ArrayList<Set<V>> evenOddVertices = new ArrayList<>(2);
evenOddVertices.add(evenVertices);
evenOddVertices.add(oddVertices);
return evenOddVertices;
}
/*
* Function to obtain V_odd(X, G[c, d]) and V_even(X, G[c, d]), where X a subset of vertices and
* G[c, d] is the eligibility subgraph (as defined for the function isEligibleEdge). V_odd is the
* set of vertices reachable via an odd-length alternating path in G[c, d] starting with an unmatched
* edge that incidents a vertex in X, and V_even is the set reachable by an even-length alternating path
*
* For efficiency, the algorithm returns both V_even and V_odd as an array of vertex sets, with
* V_even at index 0 and V_odd at index 1.
*/
private List<Set<V>> getEvenOddVertices(Set<V> startingVertices, BigDecimal c, BigDecimal d) {
Set<V> visitedVertices = new HashSet<>();
Set<V> oddVertices = new HashSet<>();
Set<V> evenVertices = new HashSet<>();
for (V vertex : startingVertices) {
//depth first search
Stack<V> stack = new Stack<>();
Map<V, Integer> distance = new HashMap<>();
if (!visitedVertices.contains(vertex)) {
stack.push(vertex);
distance.put(vertex, 0);
}
while (!stack.isEmpty()) {
V v = stack.pop();
if (!visitedVertices.contains(v)) {
visitedVertices.add(v);
if (distance.get(v) % 2 == 0) {
// reachable via an even-length path
evenVertices.add(v);
} else {
// reachable via an odd-length path
oddVertices.add(v);
}
for (E edge : graph.edgesOf(v)) {
// if this is a vertex in the starting set, only consider unmatched edges
if (startingVertices.contains(v) && matching.contains(edge)) {
continue;
}
// only consider edges in G[c, d]
if (isEligibleEdge(edge, c, d)) {
V nextVertex = Graphs.getOppositeVertex(graph, edge, v);
stack.push(nextVertex);
distance.put(nextVertex, distance.get(v) + 1);
}
}
}
}
}
ArrayList<Set<V>> evenOddVertices = new ArrayList<>(2);
evenOddVertices.add(evenVertices);
evenOddVertices.add(oddVertices);
return evenOddVertices;
}
/*
* Returns the outgoing edge set of a vertex as though the graph is a directed graph,
* with edges going from partition2 to partition1 if they are unmatched and
* from partition1 to partition2 if they are matched.
*/
private Set<E> getOutgoingEdgesTranspose(V v) {
if (partition1.contains(v)) {
return graph.edgesOf(v)
.stream()
.filter(matching::contains)
.collect(Collectors.toCollection(HashSet::new));
} else {
return graph.edgesOf(v)
.stream()
.filter(e -> !matching.contains(e))
.collect(Collectors.toCollection(HashSet::new));
}
}
/*
* Returns the outgoing edge set of a vertex as though the graph is a directed graph,
* with edges going from partition1 to partition2 if they are unmatched and
* from partition2 to partition1 if they are matched.
*/
private Set<E> getOutgoingEdges(V v) {
if (partition2.contains(v)) {
return graph.edgesOf(v)
.stream()
.filter(matching::contains)
.collect(Collectors.toCollection(HashSet::new));
} else {
return graph.edgesOf(v)
.stream()
.filter(e -> !matching.contains(e))
.collect(Collectors.toCollection(HashSet::new));
}
}
/*
* Returns whether an edge is both in the eligibility subgraph G[c, d] and,
* from starting vector v, an outgoing edge of the directed graph obtained by
* orienting edges from partition1 to partition2 if they are unmatched and from
* partition2 to partition1 if they are matched.
*/
private boolean isEligibleDirectedEdge(V v, E e, BigDecimal c, BigDecimal d) {
if (partition1.contains(v) && matching.contains(e)) {
return false;
}
if (partition2.contains(v) && !matching.contains(e)) {
return false;
}
return isEligibleEdge(e, c, d);
}
/*
* Returns whether an edge is in the eligibility subgraph G[c, d]. Given an edge e, let y(e) be the sum
* of the vertex potential of the source and target of e. Let w_i(e) be the scaled weight of e (as given
* by the scaledWeight function). Then G[c, d] is defined as the subgraph of G containing all edges e
* satisfying either of the following conditions:
* * e is not in the current matching and y(e) = w_i(e)
* or
* * e is in the current matching and w_i(e) + c * delta <= y(e) <= w_i(e) + d * delta.
*/
private boolean isEligibleEdge(E e, BigDecimal c, BigDecimal d) {
BigDecimal scaledEdgeWeight = getScaledWeight(e);
BigDecimal edgePotential = getEdgePotential(e);
if (matching.contains(e)) {
boolean c_eligible = scaledEdgeWeight.add(c.multiply(delta)).compareTo(edgePotential) <= 0;
boolean d_eligible = edgePotential.compareTo(scaledEdgeWeight.add(d.multiply(delta))) <= 0;
return c_eligible && d_eligible;
}
return edgePotential.compareTo(scaledEdgeWeight) == 0;
}
/*
* Given an edge, returns whether it is in the current matching
*/
private boolean isMatchedEdge(E e) {
return matching.contains(e);
}
/*
* Given a vertex, returns whether its edge set contains a matched edge
*/
private boolean isFreeVertex(V v) {
return !matchedEdge.containsKey(v);
}
/*
* Given an edge e with weight w(e), returns the scaled weight w_i(e) of that edge,
* where w_i(e) = delta * floor(w(e) / delta).
*/
private BigDecimal getScaledWeight(E e) {
BigDecimal weight = BigDecimal.valueOf(graph.getEdgeWeight(e));
return delta.multiply(bigDecimalFloor(weight.divide(delta)));
}
/*
* Given a vertex, returns the sum of the potentials of its endpoints
*/
private BigDecimal getEdgePotential(E e) {
return potentials.get(graph.getEdgeSource(e)).add(potentials.get(graph.getEdgeTarget(e)));
}
private void incrementScale() {
if (currentScale < numberOfScales) {
System.out.println("prev scale: " + currentScale);
currentScale = currentScale + 1;
System.out.println("new scale: " + currentScale);
System.out.println("prev delta_i: " + delta);
delta = delta.divide(new BigDecimal(2));
System.out.println("new delta_i: " + delta);
}
}
// Ceiling function for BigDecimal values
private BigDecimal bigDecimalCeil(BigDecimal num) {
return num.setScale(0, RoundingMode.CEILING);
}
// Floor function for BigDecimal values
private BigDecimal bigDecimalFloor(BigDecimal num) {
return num.setScale(0, RoundingMode.FLOOR);
}
class asDirectedGraph<V, E> extends GraphDelegator<V, E>
implements Serializable, Graph<V, E> {
//edges oriented from partition1 to partition2 if not in the matching, from partition2 to partition1 if
//in the matching.
private static final String NO_EDGE_ADD = "this graph does not support edge addition";
public asDirectedGraph(Graph<V, E> g)
{
super(g);
}
/**
* {@inheritDoc}
*/
@Override
public Set<E> getAllEdges(V sourceVertex, V targetVertex)
{
Set<E> undirectedList = super.getAllEdges(sourceVertex, targetVertex);
if (undirectedList.size() == 0) {
return undirectedList;
}
boolean leftSource = partition1.contains(sourceVertex);
Set<E> directedList;
if (leftSource) {
directedList = undirectedList.stream()
.filter(e -> !matching.contains(e))
.collect(Collectors.toCollection(HashSet::new));
}
else {
directedList = undirectedList.stream()
.filter(matching::contains)
.collect(Collectors.toCollection(HashSet::new));
}
return directedList;
}
/**
* {@inheritDoc}
*/
@Override
public E getEdge(V sourceVertex, V targetVertex)
{
E edge = super.getEdge(sourceVertex, targetVertex);
if (matching.contains(edge) && partition1.contains(sourceVertex)) {
return null;
}
else if (!matching.contains(edge) && partition2.contains(sourceVertex)) {
return null;
}
else {
return edge;
}
}
/**
* {@inheritDoc}
*
* @throws UnsupportedOperationException always, since operation is unsupported
*/
@Override
public E addEdge(V sourceVertex, V targetVertex)
{
throw new UnsupportedOperationException(NO_EDGE_ADD);
}
/**
* {@inheritDoc}
*
* @throws UnsupportedOperationException always, since operation is unsupported
*/
@Override
public boolean addEdge(V sourceVertex, V targetVertex, E e)
{
throw new UnsupportedOperationException(NO_EDGE_ADD);
}
/**
* {@inheritDoc}
*/
@Override
public int degreeOf(V vertex)
{
return super.degreeOf(vertex);
}
/**
* {@inheritDoc}
*/
@Override
public Set<E> incomingEdgesOf(V vertex)
{
Set<E> incomingEdges = super.edgesOf(vertex);
if (partition1.contains(vertex)) {
return incomingEdges.stream()
.filter(matching::contains)
.collect(Collectors.toCollection(HashSet::new));
}
else {
return incomingEdges.stream()
.filter(a -> !matching.contains(a))
.collect(Collectors.toCollection(HashSet::new));
}
}
/**
* {@inheritDoc}
*/
@Override
public int inDegreeOf(V vertex)
{
return incomingEdgesOf(vertex).size();
}
/**
* {@inheritDoc}
*/
@Override
public Set<E> outgoingEdgesOf(V vertex)
{
Set<E> outgoingEdges = super.edgesOf(vertex);
if (partition2.contains(vertex)) {
return outgoingEdges.stream()
.filter(matching::contains)
.collect(Collectors.toCollection(HashSet::new));
}
else {
return outgoingEdges.stream()
.filter(a -> !matching.contains(a))
.collect(Collectors.toCollection(HashSet::new));
}
}
/**
* {@inheritDoc}
*/
@Override
public int outDegreeOf(V vertex)
{
return super.outgoingEdgesOf(vertex).size();
}
public GraphType getType() {
return super.getType().asDirected();
}
}
class asTransposeDirectedGraph extends asDirectedGraph {
private final Set<V> partition1;
private final Set<V> partition2;
public asTransposeDirectedGraph(Graph<V, E> g, Set<V> partition1, Set<V> partition2) {
super(g);
this.partition1 = partition2;
this.partition2 = partition1;
}
}
}