blob: 5b33a2255051baff5dd700f11b3213dffb12f767 [file] [log] [blame]
/*
* Copyright 2012-present Facebook, 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.facebook.buck.graph;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.Collections;
import java.util.Deque;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Represents a directed graph with unweighted edges. For a given source and sink node pair, there
* is at most one directed edge connecting them in the graph.
* The graph is not required to be connected or acyclic.
* @param <T> the type of object stored as nodes in this graph
*/
public final class MutableDirectedGraph<T> {
/**
* It is possible to have a node in the graph without any edges, which is why we must maintain a
* separate collection for nodes in the graph, rather than just using the keySet of
* {@link #outgoingEdges}.
*/
private final Set<T> nodes;
/**
* Represents the edges in the graph.
* Keys are source nodes; values are corresponding sync nodes.
*/
private final HashMultimap<T, T> outgoingEdges;
/**
* Represents the edges in the graph.
* Keys are sink nodes; values are corresponding source nodes.
*/
private final HashMultimap<T, T> incomingEdges;
/**
* Creates a new graph with no nodes or edges.
*/
public MutableDirectedGraph() {
this.nodes = Sets.newHashSet();
this.outgoingEdges = HashMultimap.create();
this.incomingEdges = HashMultimap.create();
}
/**
* Creates a mutable copy of the specified graph.
*/
public MutableDirectedGraph(MutableDirectedGraph<T> graph) {
Preconditions.checkNotNull(graph);
this.nodes = Sets.newHashSet(graph.nodes);
this.outgoingEdges = HashMultimap.create(graph.outgoingEdges);
this.incomingEdges = HashMultimap.create(graph.incomingEdges);
}
/** @return the number of nodes in the graph */
public int getNodeCount() {
return nodes.size();
}
/** @return the number of edges in the graph */
public int getEdgeCount() {
return outgoingEdges.size();
}
/** @return whether the specified node is present in the graph */
public boolean containsNode(T node) {
Preconditions.checkNotNull(node);
return nodes.contains(node);
}
/** @return whether an edge from the source to the sink is present in the graph */
public boolean containsEdge(T source, T sink) {
Preconditions.checkNotNull(source);
Preconditions.checkNotNull(sink);
return outgoingEdges.containsEntry(source, sink);
}
/**
* Adds the specified node to the graph.
*/
public boolean addNode(T node) {
Preconditions.checkNotNull(node);
return nodes.add(node);
}
/**
* Removes the specified node from the graph.
*/
public boolean removeNode(T node) {
Preconditions.checkNotNull(node);
boolean isRemoved = nodes.remove(node);
Set<T> nodesReachableFromTheSpecifiedNode = outgoingEdges.removeAll(node);
for (T reachableNode : nodesReachableFromTheSpecifiedNode) {
incomingEdges.remove(reachableNode, node);
}
return isRemoved;
}
/**
* Adds an edge between {@code source} and {@code sink}. Adds the nodes to the graph if they are
* not already present.
*/
public void addEdge(T source, T sink) {
Preconditions.checkNotNull(source);
Preconditions.checkNotNull(sink);
nodes.add(source);
nodes.add(sink);
outgoingEdges.put(source, sink);
incomingEdges.put(sink, source);
}
/**
* Removes the edge between {@code source} and {@code sink}. This does not remove {@code source}
* or {@code sink} from the graph. Note that this may leave either {@code source} or
* {@code sink} as unconnected nodes in the graph.
*/
public void removeEdge(T source, T sink) {
Preconditions.checkNotNull(source);
Preconditions.checkNotNull(sink);
outgoingEdges.remove(source, sink);
incomingEdges.remove(sink, source);
}
public ImmutableSet<T> getOutgoingNodesFor(T source) {
return ImmutableSet.copyOf(outgoingEdges.get(source));
}
public ImmutableSet<T> getIncomingNodesFor(T sink) {
return ImmutableSet.copyOf(incomingEdges.get(sink));
}
public boolean hasIncomingEdges(T node) {
Preconditions.checkNotNull(node);
return this.incomingEdges.containsKey(node);
}
/** @return an unmodifiable view of the nodes in this graph */
public Set<T> getNodes() {
return Collections.unmodifiableSet(nodes);
}
public boolean isAcyclic() {
return findCycles().isEmpty();
}
public ImmutableSet<ImmutableSet<T>> findCycles() {
Set<Set<T>> cycles = Sets.filter(tarjan(), new Predicate<Set<T>>() {
@Override
public boolean apply(@Nullable Set<T> stronglyConnectedComponent) {
return stronglyConnectedComponent.size() > 1;
}
});
Iterable<ImmutableSet<T>> immutableCycles = Iterables.transform(cycles, new Function<Set<T>, ImmutableSet<T>>() {
@Override
public ImmutableSet<T> apply(Set<T> cycle) {
return ImmutableSet.copyOf(cycle);
}
});
// Tarjan's algorithm (as pseudo-coded on Wikipedia) does not appear to account for single-node
// cycles. Therefore, we must check for them exclusively.
ImmutableSet.Builder<ImmutableSet<T>> builder = ImmutableSet.builder();
builder.addAll(immutableCycles);
for (T node : nodes) {
if (containsEdge(node, node)) {
builder.add(ImmutableSet.of(node));
}
}
return builder.build();
}
/**
* Implementation of
* http://en.wikipedia.org/wiki/Tarjan%E2%80%99s_strongly_connected_components_algorithm
* used to find cycles in the graph.
*/
private Set<Set<T>> tarjan() {
Tarjan<T> tarjan = new Tarjan<T>(this);
return tarjan.findStronglyConnectedComponents();
}
public ImmutableSet<T> getNodesWithNoIncomingEdges() {
return ImmutableSet.copyOf(Sets.difference(nodes, incomingEdges.keySet()));
}
public ImmutableSet<T> getNodesWithNoOutgoingEdges() {
return ImmutableSet.copyOf(Sets.difference(nodes, outgoingEdges.keySet()));
}
private static class Tarjan<S> {
private final MutableDirectedGraph<S> graph;
private final Map<S, Integer> indexes;
private final Map<S, Integer> lowlinks;
private final Deque<S> nodeStack;
private final Set<Set<S>> stronglyConnectedComponents;
private int index;
private Tarjan(MutableDirectedGraph<S> graph) {
this.graph = graph;
this.indexes = Maps.newHashMap();
this.lowlinks = Maps.newHashMap();
this.nodeStack = Lists.newLinkedList();
this.stronglyConnectedComponents = Sets.newHashSet();
this.index = 0;
}
public Set<Set<S>> findStronglyConnectedComponents() {
for (S node : graph.nodes) {
if (!indexes.containsKey(node)) {
doStrongConnect(node);
}
}
return stronglyConnectedComponents;
}
private void doStrongConnect(final S node) {
// Set the depth index for node to the smallest unused index.
indexes.put(node, index);
lowlinks.put(node, index);
index++;
nodeStack.push(node);
// Consider successors of node.
for (S sink : graph.getOutgoingNodesFor(node)) {
if (!indexes.containsKey(sink)) {
doStrongConnect(sink);
int lowlink = Math.min(lowlinks.get(node), lowlinks.get(sink));
lowlinks.put(node, lowlink);
} else if (nodeStack.contains(sink)) {
// TODO(mbolin): contains() is O(N), consider maintaining an index so it is O(1)?
int lowlink = Math.min(lowlinks.get(node), indexes.get(sink));
lowlinks.put(node, lowlink);
}
}
// If node is a root node, then pop the stack and generate a strongly connected component.
if (lowlinks.get(node).equals(indexes.get(node))) {
Set<S> stronglyConnectedComponent = Sets.newHashSet();
S componentElement;
do {
componentElement = nodeStack.pop();
stronglyConnectedComponent.add(componentElement);
} while (componentElement != node);
stronglyConnectedComponents.add(stronglyConnectedComponent);
}
}
}
}