/* * Copyright (c) 2011 Google, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.common.truth; import com.google.common.base.Optional; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.ImmutableBiMap; import com.google.common.collect.Multimap; import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.ArrayDeque; import java.util.HashMap; import java.util.Map; import java.util.Queue; /** * Helper routines related to <a href="https://en.wikipedia.org/wiki/Matching_(graph_theory)">graph * matchings</a>. * * @author Pete Gillin */ final class GraphMatching { /** * Finds a <a * href="https://en.wikipedia.org/wiki/Matching_(graph_theory)#In_unweighted_bipartite_graphs"> * maximum cardinality matching of a bipartite graph</a>. The vertices of one part of the * bipartite graph are identified by objects of type {@code U} using object equality. The vertices * of the other part are similarly identified by objects of type {@code V}. The input bipartite * graph is represented as a {@code Multimap<U, V>}: each entry represents an edge, with the key * representing the vertex in the first part and the value representing the value in the second * part. (Note that, even if {@code U} and {@code V} are the same type, equality between a key and * a value has no special significance: effectively, they are in different domains.) Fails if any * of the vertices (keys or values) are null. The output matching is similarly represented as a * {@code BiMap<U, V>} (the property that a matching has no common vertices translates into the * bidirectional uniqueness property of the {@link BiMap}). * * <p>If there are multiple matchings which share the maximum cardinality, an arbitrary one is * returned. */ static <U, V> ImmutableBiMap<U, V> maximumCardinalityBipartiteMatching(Multimap<U, V> graph) { return HopcroftKarp.overBipartiteGraph(graph).perform(); } private GraphMatching() {} /** * Helper which implements the <a * href="https://en.wikipedia.org/wiki/Hopcroft%E2%80%93Karp_algorithm">Hopcroft–Karp</a> * algorithm. * * <p>The worst-case complexity is {@code O(E V^0.5)} where the graph contains {@code E} edges and * {@code V} vertices. For dense graphs, where {@code E} is {@code O(V^2)}, this is {@code V^2.5} * (and non-dense graphs perform better than dense graphs with the same number of vertices). */ private static class HopcroftKarp<U, V> { private final Multimap<U, V> graph; /** * Factory method which returns an instance ready to perform the algorithm over the bipartite * graph described by the given multimap. */ static <U, V> HopcroftKarp<U, V> overBipartiteGraph(Multimap<U, V> graph) { return new HopcroftKarp<U, V>(graph); } private HopcroftKarp(Multimap<U, V> graph) { this.graph = graph; } /** Performs the algorithm, and returns a bimap describing the matching found. */ ImmutableBiMap<U, V> perform() { BiMap<U, V> matching = HashBiMap.create(); while (true) { // Perform the BFS as described below. This finds the length of the shortest augmenting path // and a guide which locates all the augmenting paths of that length. Map<U, Integer> layers = new HashMap<U, Integer>(); Optional<Integer> freeRhsVertexLayer = breadthFirstSearch(matching, layers); if (!freeRhsVertexLayer.isPresent()) { // The BFS failed, i.e. we found no augmenting paths. So we're done. break; } // Perform the DFS and update the matching as described below starting from each free LHS // vertex. This finds a disjoint set of augmenting paths of the shortest length and updates // the matching by computing the symmetric difference with that set. for (U lhs : graph.keySet()) { if (!matching.containsKey(lhs)) { depthFirstSearch(matching, layers, freeRhsVertexLayer.get(), lhs); } } } return ImmutableBiMap.copyOf(matching); } /** * Performs the Breadth-First Search phase of the algorithm. Specifically, treats the bipartite * graph as a directed graph where every unmatched edge (i.e. every edge not in the current * matching) is directed from the LHS vertex to the RHS vertex and every matched edge is * directed from the RHS vertex to the LHS vertex, and performs a BFS which starts from all of * the free LHS vertices (i.e. the LHS vertices which are not in the current matching) and stops * either at the end of a layer where a free RHS vertex is found or when the search is exhausted * if no free RHS vertex is found. Keeps track of which layer of the BFS each LHS vertex was * found in (for those LHS vertices visited during the BFS), so the free LHS vertices are in * layer 1, those reachable by following an unmatched edge from any free LHS vertex to any * non-free RHS vertex and then the matched edge back to a LHS vertex are in layer 2, etc. Note * that every path in a successful search starts with a free LHS vertex and ends with a free RHS * vertex, with every intermediate vertex being non-free. * * @param matching A bimap describing the matching to be used for the BFS, which is not modified * by this method * @param layers A map to be filled with the layer of each LHS vertex visited during the BFS, * which should be empty when passed into this method and will be modified by this method * @return The number of the layer in which the first free RHS vertex was found, if any, and the * absent value if the BFS was exhausted without finding any free RHS vertex */ private Optional<Integer> breadthFirstSearch(BiMap<U, V> matching, Map<U, Integer> layers) { Queue<U> queue = new ArrayDeque<U>(); Optional<Integer> freeRhsVertexLayer = Optional.absent(); // Enqueue all free LHS vertices and assign them to layer 1. for (U lhs : graph.keySet()) { if (!matching.containsKey(lhs)) { layers.put(lhs, 1); queue.add(lhs); } } // Now proceed with the BFS. while (!queue.isEmpty()) { U lhs = queue.remove(); int layer = layers.get(lhs); // If the BFS has proceeded past a layer in which a free RHS vertex was found, stop. if (freeRhsVertexLayer.isPresent() && layer > freeRhsVertexLayer.get()) { break; } // We want to consider all the unmatched edges from the current LHS vertex to the RHS, and // then all the matched edges from those RHS vertices back to the LHS, to find the next // layer of LHS vertices. We actually iterate over all edges, both matched and unmatched, // from the current LHS vertex: we'll just do nothing for matched edges. for (V rhs : graph.get(lhs)) { if (!matching.containsValue(rhs)) { // We found a free RHS vertex. Record the layer at which we found it. Since the RHS // vertex is free, there is no matched edge to follow. (Note that the edge from the LHS // to the RHS must be unmatched, because a matched edge cannot lead to a free vertex.) if (!freeRhsVertexLayer.isPresent()) { freeRhsVertexLayer = Optional.of(layer); } } else { // We found an RHS vertex with a matched vertex back to the LHS. If we haven't visited // that new LHS vertex yet, add it to the next layer. (If the edge from the LHS to the // RHS was matched then the matched edge from the RHS to the LHS will lead back to the // current LHS vertex, which has definitely been visited, so we correctly do nothing.) U nextLhs = matching.inverse().get(rhs); if (!layers.containsKey(nextLhs)) { layers.put(nextLhs, layer + 1); queue.add(nextLhs); } } } } return freeRhsVertexLayer; } /** * Performs the Depth-First Search phase of the algorithm. The DFS is guided by the BFS phase, * i.e. it only uses paths which were used in the BFS. That means the steps in the DFS proceed * from an LHS vertex via an unmatched edge to an RHS vertex and from an RHS vertex via a * matched edge to an LHS vertex only if that LHS vertex is one layer deeper in the BFS than the * previous one. It starts from the specified LHS vertex and stops either when it finds one of * the free RHS vertices located by the BFS or when the search is exhausted. If a free RHS * vertex is found then all the unmatched edges in the search path and added to the matching and * all the matched edges in the search path are removed from the matching; in other words, the * direction (which is determined by the matched/unmatched status) of every edge in the search * path is flipped. Note several properties of this update to the matching: * * <ul> * <li>Because the search path must contain one more unmatched than matched edges, the effect of * this modification is to increase the size of the matching by one. * <li>This modification results in the free LHS vertex at the start of the path and the free * RHS vertex at the end of the path becoming non-free, while the intermediate non-free * vertices stay non-free. * <li>None of the edges used in this search path may be used in any further DFS. They cannot be * used in the same direction as they were in this DFS because their directions are flipped; * and they cannot be used in their new directions because we only use edges leading to the * next layer of the BFS and, after flipping the directions, these edges now lead to the * previous layer. * <li>As a consequence of the previous property, repeated invocations of this method will find * only paths which were used in the BFS and which were not used in any previous DFS (i.e. * the set of edges used in the paths found by repeated DFSes are disjoint). * </ul> * * @param matching A bimap describing the matching to be used for the BFS, which will be * modified by this method as described above * @param layers A map giving the layer of each LHS vertex visited during the BFS, which will * not be modified by this method * @param freeRhsVertexLayer The number of the layer in which the first free RHS vertex was * found * @param lhs The LHS vertex from which to start the DFS * @return Whether or not the DFS was successful */ @CanIgnoreReturnValue private boolean depthFirstSearch( BiMap<U, V> matching, Map<U, Integer> layers, int freeRhsVertexLayer, U lhs) { // Note that this differs from the method described in the text of the wikipedia article (at // time of writing) in two ways. Firstly, we proceed from a free LHS vertex to a free RHS // vertex in the target layer instead of the other way around, which makes no difference. // Secondly, we update the matching using the path found from each DFS after it is found, // rather than using all the paths at the end of the phase. As explained above, the effect of // this is that we automatically find only the disjoint set of paths, as required. This is, // fact, the approach taken in the pseudocode of the wikipedia article (at time of writing). int layer = layers.get(lhs); if (layer > freeRhsVertexLayer) { // We've gone past the target layer, so we're not going to find what we're looking for. return false; } // Consider every edge from this LHS vertex. for (V rhs : graph.get(lhs)) { if (!matching.containsValue(rhs)) { // We found a free RHS vertex. (This must have been in the target layer because, by // definition, no free RHS vertex is reachable in any earlier layer, and because we stop // when we get past that layer.) We add the unmatched edge used to get here to the // matching, and remove any previous matched edge leading to the LHS vertex. matching.forcePut(lhs, rhs); return true; } else { // We found a non-free RHS vertex. Follow the matched edge from that RHS vertex to find // the next LHS vertex. U nextLhs = matching.inverse().get(rhs); if (layers.containsKey(nextLhs) && layers.get(nextLhs) == layer + 1) { // The next LHS vertex is in the next layer of the BFS, so we can use this path for our // DFS. Recurse into the DFS. if (depthFirstSearch(matching, layers, freeRhsVertexLayer, nextLhs)) { // The DFS succeeded, and we're reversing back up the search path. At each stage we // put the unmatched edge from the LHS to the RHS into the matching, and remove any // matched edge previously leading to the LHS. The combined effect of all the // modifications made while reversing all the way back up the search path is to update // the matching as described in the javadoc. matching.forcePut(lhs, rhs); return true; } } } } return false; } } }