diff --git a/src/main/java/MaximumWeightBipartiteAuctionMatching.java b/src/main/java/MaximumWeightBipartiteAuctionMatching.java new file mode 100644 index 0000000..b5ebd84 --- /dev/null +++ b/src/main/java/MaximumWeightBipartiteAuctionMatching.java @@ -0,0 +1,130 @@ +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, found using the + + */ + +public class MaximumWeightBipartiteAuctionMatching implements MatchingAlgorithm { + + private final Graph graph; + private final Set partition1; + private final Set partition2; + private final BigDecimal delta; + private Set matching; + private BigDecimal matchingWeight; + + public MaximumWeightBipartiteAuctionMatching(Graph graph, Set partition1, Set 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.delta = BigDecimal.valueOf(1 / ((double) n + 1)); + this.matching = new LinkedHashSet<>(); + this.matchingWeight = BigDecimal.ZERO; + } + + + /* + Method coded using MaximumWeightBipartiteMatching.class from JgraphT as a model + */ + @Override + public Matching 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 mus be the smaller of the two paritions. + */ + Set items; + Set bidders; + if (partition2.size() >= partition1.size()) { + bidders = partition1; + items = partition2; + } + else { + bidders = partition2; + items = partition1; + } + + /* + 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. + */ + Map owners = new HashMap<>(); + Map prices = new HashMap<>(); + for(V item: items) { + owners.put(item, null); + prices.put(item, BigDecimal.ZERO); + } + + //Initialize queue of all bidders that don't currently own an item + Queue unmatchedBidders = new ArrayDeque<>(); + for(V bidder: bidders) { + unmatchedBidders.offer(bidder); + } + + while (unmatchedBidders.size() > 0) { + V bidder = unmatchedBidders.poll(); + V item = null; + BigDecimal bestValue = BigDecimal.valueOf(-1.0); + //find the item that offers the best value for this bidder + for (E edge: graph.edgesOf(bidder)) { + V tmp = getItem(bidder, edge); + BigDecimal value = BigDecimal.valueOf(graph.getEdgeWeight(edge)).subtract(prices.get(tmp)); + if (value.compareTo(bestValue) >= 0) { + bestValue = value; + item = tmp; + } + } + if(bestValue.compareTo(BigDecimal.ZERO) >= 0) { + V formerOwner = owners.get(item); + BigDecimal formerPrice = prices.get(item); + if (formerOwner != null) { + unmatchedBidders.offer(formerOwner); + } + owners.put(item, bidder); + prices.put(item, formerPrice.add(delta)); + } + } + for (V item: owners.keySet()) { + if (owners.get(item) != null) { + //hopefully which vertex is "source" and which is "target" will be irrelevant here + matching.add(graph.getEdge(item, owners.get(item))); + } + } + + for(E edge: matching) { + matchingWeight.add(BigDecimal.valueOf(graph.getEdgeWeight(edge))); + } + + + return new MatchingImpl<>(graph, matching, matchingWeight.doubleValue()); + } + + /* + There may be a better way to do this, I just don't know which vertex will be the "source" and + which will be the "target", so I'm using this function to make sure I get the right one. + */ + private V getItem(V bidder, E edge) { + if (graph.getEdgeSource(edge).equals(bidder)) { + return graph.getEdgeTarget(edge); + } + else { + return graph.getEdgeSource(edge); + } + } +}