Files
BiGpairSEQ/src/main/java/MaximumIntegerWeightBipartiteAuctionMatching.java

178 lines
6.7 KiB
Java

import org.jgrapht.Graph;
import org.jgrapht.GraphTests;
import org.jgrapht.alg.interfaces.MatchingAlgorithm;
import java.math.BigDecimal;
import java.util.*;
/**
* Maximum weight matching in bipartite graphs with strictly integer edge weights, using a forward auction algorithm.
* This implementation uses the Gauss-Seidel version of the forward auction algorithm, in which bids are submitted
* one at a time. For any weighted bipartite graph with n vertices in the smaller partition, this algorithm will produce
* a matching that is within n*epsilon of being optimal. Using an epsilon = 1/(n+1) ensures that this matching differs
* from an optimal matching by <1. Thus, for a bipartite graph with strictly integer weights, this algorithm returns
* a maximum weight matching.
*
* See:
* "Towards auction algorithms for large dense assignment problems"
* Libor Buš and Pavel Tvrdík, Comput Optim Appl (2009) 43:411-436
* https://link.springer.com/article/10.1007/s10589-007-9146-5
*
* See also:
* Many books and papers by Dimitri Bertsekas, including chapter 4 of Linear Network Optimization:
* https://web.mit.edu/dimitrib/www/LNets_Full_Book.pdf
*
* @param <V> the graph vertex type
* @param <E> the graph edge type
*
* @author Eugene Fischer
*/
public class MaximumIntegerWeightBipartiteAuctionMatching<V, E> implements MatchingAlgorithm<V, E> {
private final Graph<V, E> graph;
private final Set<V> partition1;
private final Set<V> partition2;
private final BigDecimal epsilon;
private final Set<E> matching;
private BigDecimal matchingWeight;
private boolean swappedPartitions = false;
public MaximumIntegerWeightBipartiteAuctionMatching(Graph<V, E> graph, Set<V> partition1, Set<V> partition2) {
this.graph = GraphTests.requireUndirected(graph);
this.partition1 = Objects.requireNonNull(partition1, "Partition 1 cannot be null");
this.partition2 = Objects.requireNonNull(partition2, "Partition 2 cannot be null");
int n = Math.max(partition1.size(), partition2.size());
this.epsilon = BigDecimal.valueOf(1 / ((double) n + 1)); //The minimum price increase of a bid
this.matching = new LinkedHashSet<>();
this.matchingWeight = BigDecimal.ZERO;
}
/*
Method coded using MaximumWeightBipartiteMatching.class from JgraphT as a model
*/
@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");
}
/*
If the two partitions are different sizes, the bidders must be the smaller of the two partitions.
*/
Set<V> items;
Set<V> bidders;
if (partition2.size() >= partition1.size()) {
bidders = partition1;
items = partition2;
}
else {
bidders = partition2;
items = partition1;
swappedPartitions = true;
}
/*
Create a map to track the owner of each item, which is initially null,
and a map to track the price of each item, which is initially 0. An
Initial price of 0 allows for asymmetric assignment (though does mean
that this form of the algorithm cannot take advantage of epsilon-scaling).
*/
Map<V, V> owners = new HashMap<>();
Map<V, BigDecimal> prices = new HashMap<>();
for(V item: items) {
owners.put(item, null);
prices.put(item, BigDecimal.ZERO);
}
//Create a queue of bidders that don't currently own an item, which is initially all of them
Queue<V> unmatchedBidders = new ArrayDeque<>();
for(V bidder: bidders) {
unmatchedBidders.offer(bidder);
}
//Run the auction while there are remaining unmatched bidders
while (unmatchedBidders.size() > 0) {
V bidder = unmatchedBidders.poll();
V item = null;
BigDecimal bestValue = BigDecimal.valueOf(-1.0);
BigDecimal runnerUpValue = BigDecimal.valueOf(-1.0);
/*
Find the items that offer the best and second-best value for the bidder,
then submit a bid equal to the price of the best-valued item plus the marginal value over
the second-best-valued item plus epsilon.
*/
for (E edge: graph.edgesOf(bidder)) {
double weight = graph.getEdgeWeight(edge);
if(weight == 0.0) {
continue;
}
V tmp = getItem(edge);
BigDecimal value = BigDecimal.valueOf(weight).subtract(prices.get(tmp));
if (value.compareTo(bestValue) >= 0) {
runnerUpValue = bestValue;
bestValue = value;
item = tmp;
}
else if (value.compareTo(runnerUpValue) >= 0) {
runnerUpValue = value;
}
}
if(bestValue.compareTo(BigDecimal.ZERO) >= 0) {
V formerOwner = owners.get(item);
BigDecimal price = prices.get(item);
BigDecimal bid = price.add(bestValue).subtract(runnerUpValue).add(epsilon);
if (formerOwner != null) {
unmatchedBidders.offer(formerOwner);
}
owners.put(item, bidder);
prices.put(item, bid);
}
}
//Add all edges between items and their owners to the matching
for (V item: owners.keySet()) {
if (owners.get(item) != null) {
matching.add(graph.getEdge(item, owners.get(item)));
}
}
//Sum the edges of the matching to obtain the matching weight
for(E edge: matching) {
this.matchingWeight = this.matchingWeight.add(BigDecimal.valueOf(graph.getEdgeWeight(edge)));
}
return new MatchingImpl<>(graph, matching, matchingWeight.doubleValue());
}
private V getItem(E edge) {
if (swappedPartitions) {
return graph.getEdgeSource(edge);
}
else {
return graph.getEdgeTarget(edge);
}
}
// //method for implementing a forward-reverse auction algorithm, not used here
// private V getBidder(E edge) {
// if (swappedPartitions) {
// return graph.getEdgeTarget(edge);
// }
// else {
// return graph.getEdgeSource(edge);
// }
// }
public BigDecimal getMatchingWeight() {
return matchingWeight;
}
}