org.jgrapht.alg.decomposition.DulmageMendelsohnDecomposition Maven / Gradle / Ivy
/*
* (C) Copyright 2018-2023, by CAE Tech Limited 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.decomposition;
import org.jgrapht.*;
import org.jgrapht.alg.connectivity.*;
import org.jgrapht.alg.interfaces.*;
import org.jgrapht.alg.interfaces.MatchingAlgorithm.*;
import org.jgrapht.alg.matching.*;
import org.jgrapht.graph.*;
import org.jgrapht.graph.builder.*;
import org.jgrapht.traverse.*;
import java.util.*;
/**
*
* This class computes a Dulmage-Mendelsohn Decomposition of a bipartite graph. A Dulmage–Mendelsohn
* decomposition is a partition of the vertices of a bipartite graph into subsets, with the property
* that two adjacent vertices belong to the same subset if and only if they are paired with each
* other in a perfect matching of the graph. This particular implementation is capable of computing
* both a coarse and a fine Dulmage-Mendelsohn Decomposition.
*
*
*
* The Dulmage-Mendelsohn Decomposition is based on a maximum-matching of the graph $G$. This
* implementation uses the Hopcroft-Karp maximum matching algorithm by default.
*
*
*
* A coarse Dulmage-Mendelsohn Decomposition is a partitioning into three subsets. Where $D$ is the
* set of vertices in G that are not matched in the maximum matching of $G$, these subsets are:
*
*
* - The vertices in $D \cap U$ and their neighbors
* - The vertices in $D \cap V$ and their neighbors
* - The remaining vertices
*
*
*
* A fine Dulmage-Mendelsohn Decomposition further partitions the remaining vertices into
* strongly-connected sets. This implementation uses Kosaraju's algorithm for the
* strong-connectivity analysis.
*
*
*
* The Dulmage-Mendelsohn Decomposition was introduced in:
* Dulmage, A.L., Mendelsohn, N.S. Coverings of bipartitegraphs, Canadian J. Math., 10, 517-534,
* 1958.
*
*
*
* The implementation of this class is based on:
* Bunus P., Fritzson P., Methods for Structural Analysis and Debugging of Modelica Models, 2nd
* International Modelica Conference 2002
*
*
*
* The runtime complexity of this class is $O(V + E)$.
*
*
* @author Peter Harman
* @param Vertex type
* @param Edge type
*/
public class DulmageMendelsohnDecomposition
{
private final Graph graph;
private final Set partition1;
private final Set partition2;
/**
* Construct the algorithm for a given bipartite graph $G=(V_1,V_2,E)$ and it's partitions $V_1$
* and $V_2$, where $V_1\cap V_2=\emptyset$.
*
* @param graph bipartite graph
* @param partition1 the first partition, $V_1$, of vertices in the bipartite graph
* @param partition2 the second partition, $V_2$, of vertices in the bipartite graph
*/
public DulmageMendelsohnDecomposition(Graph graph, Set partition1, Set partition2)
{
this.graph = Objects.requireNonNull(graph);
this.partition1 = partition1;
this.partition2 = partition2;
assert GraphTests.isBipartite(graph);
}
/**
* Perform the decomposition, using the Hopcroft-Karp maximum-cardinality matching algorithm to
* perform the matching.
*
* @param fine true if the fine decomposition is required, false if the coarse decomposition is
* required
* @return the {@link Decomposition}
*/
public Decomposition getDecomposition(boolean fine)
{
// Get a maximum matching to the bipartite problem
HopcroftKarpMaximumCardinalityBipartiteMatching hopkarp =
new HopcroftKarpMaximumCardinalityBipartiteMatching<>(graph, partition1, partition2);
Matching matching = hopkarp.getMatching();
return decompose(matching, fine);
}
/**
* Perform the decomposition, using a pre-calculated bipartite matching
*
* @param matching the matching from a {@link MatchingAlgorithm}
* @param fine true if the fine decomposition is required
* @return the {@link Decomposition}
*/
public Decomposition decompose(Matching matching, boolean fine)
{
// Determine the unmatched vertices from both partitions
Set unmatched1 = new HashSet<>();
Set unmatched2 = new HashSet<>();
getUnmatched(matching, unmatched1, unmatched2);
// Assemble a directed graph
Graph dg = asDirectedGraph(matching);
// Find the non-square subgraph dominated by partition1
Set subset1 = new HashSet<>();
unmatched1.stream().map((v) -> {
subset1.add(v);
return v;
}).map((v) -> new DepthFirstIterator<>(dg, v)).forEachOrdered((it) -> {
while (it.hasNext()) {
subset1.add(it.next());
}
});
// Find the non-square subgraph dominated by partition2
Graph gd = new EdgeReversedGraph<>(dg);
Set subset2 = new HashSet<>();
unmatched2.stream().map((v) -> {
subset2.add(v);
return v;
}).map((v) -> new DepthFirstIterator<>(gd, v)).forEachOrdered((it) -> {
while (it.hasNext()) {
subset2.add(it.next());
}
});
// Find the square subgraph
Set subset3 = new HashSet<>();
subset3.addAll(partition1);
subset3.addAll(partition2);
subset3.removeAll(subset1);
subset3.removeAll(subset2);
if (fine) {
List> out = new ArrayList<>();
// Build a directed graph between edges of the matching in subset3
Graph graphH = asDirectedEdgeGraph(matching, subset3);
// Perform strongly-connected-components on the graph
StrongConnectivityAlgorithm sci =
new KosarajuStrongConnectivityInspector<>(graphH);
// Divide into sets of vertices
for (Set edgeSet : sci.stronglyConnectedSets()) {
Set vertexSet = new HashSet<>();
edgeSet.stream().map((edge) -> {
vertexSet.add(graph.getEdgeSource(edge));
return edge;
}).forEachOrdered((edge) -> {
vertexSet.add(graph.getEdgeTarget(edge));
});
out.add(vertexSet);
}
return new Decomposition<>(subset1, subset2, out);
} else {
return new Decomposition<>(subset1, subset2, Collections.singletonList(subset3));
}
}
/**
* The output of a decomposition operation
*
* @param vertex type
* @param edge type
*/
public static class Decomposition
{
private final Set subset1;
private final Set subset2;
private final List> subset3;
Decomposition(Set subset1, Set subset2, List> subset3)
{
this.subset1 = subset1;
this.subset2 = subset2;
this.subset3 = subset3;
}
/**
* Gets the subset dominated by partition1. Where $D$ is the set of vertices in $G$ that are
* not matched in the maximum matching of $G$, this set contains members of the first
* partition and vertices from the second partition that neighbour them.
*
* @return The vertices in $D \cap V_1$ and their neighbours
*/
public Set getPartition1DominatedSet()
{
return subset1;
}
/**
* Gets the subset dominated by partition2. Where $D$ is the set of vertices in $G$ that are
* not matched in the maximum matching of $G$, this set contains members of the second
* partition and vertices from the first partition that neighbour them.
*
* @return The vertices in $D \cap V_2$ and their neighbours
*/
public Set getPartition2DominatedSet()
{
return subset2;
}
/**
* Gets the remaining subset, or subsets in the fine decomposition. This set contains
* vertices that are matched in the maximum matching of the graph $G$. If the fine
* decomposition was used, this will be multiple sets, each a strongly-connected-component
* of the matched subset of $G$.
*
* @return List of Sets of vertices in the subsets
*/
public List> getPerfectMatchedSets()
{
return subset3;
}
}
private void getUnmatched(Matching matching, Set unmatched1, Set unmatched2)
{
unmatched1.addAll(partition1);
unmatched2.addAll(partition2);
matching.forEach((e) -> {
V source = graph.getEdgeSource(e);
V target = graph.getEdgeTarget(e);
if (partition1.contains(source)) {
unmatched1.remove(source);
unmatched2.remove(target);
} else {
unmatched2.remove(source);
unmatched1.remove(target);
}
});
}
private Graph asDirectedGraph(Matching matching)
{
GraphBuilder> builder =
DefaultDirectedGraph.createBuilder(DefaultEdge.class);
graph.vertexSet().forEach((v) -> {
builder.addVertex(v);
});
graph.edgeSet().forEach((e) -> {
V v1 = graph.getEdgeSource(e);
V v2 = graph.getEdgeTarget(e);
if (partition1.contains(v1)) {
builder.addEdge(v1, v2);
if (matching.getEdges().contains(e)) {
builder.addEdge(v2, v1);
}
} else {
builder.addEdge(v2, v1);
if (matching.getEdges().contains(e)) {
builder.addEdge(v1, v2);
}
}
});
return builder.build();
}
private Graph asDirectedEdgeGraph(Matching matching, Set subset)
{
GraphBuilder> graphHBuilder =
DefaultDirectedGraph.createBuilder(DefaultEdge.class);
for (E e : graph.edgeSet()) {
V v1 = graph.getEdgeSource(e);
V v2 = graph.getEdgeTarget(e);
if (subset.contains(v1) && subset.contains(v2)) {
if (matching.getEdges().contains(e)) {
graphHBuilder.addVertex(e);
} else {
E e1 = null;
E e2 = null;
for (E other : graph.edgesOf(v1)) {
if (matching.getEdges().contains(other)) {
e1 = other;
graphHBuilder.addVertex(e1);
break;
}
}
for (E other : graph.edgesOf(v2)) {
if (matching.getEdges().contains(other)) {
e2 = other;
graphHBuilder.addVertex(e2);
break;
}
}
graphHBuilder.addEdge(e1, e2);
}
}
}
return graphHBuilder.build();
}
}