cdc.graphs.core.SlimGraph Maven / Gradle / Ivy
package cdc.graphs.core;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import cdc.graphs.EdgeDirection;
import cdc.graphs.GraphAdapter;
import cdc.graphs.TraversalMethod;
import cdc.graphs.TraversalOrder;
import cdc.util.function.Evaluation;
import cdc.util.function.Evaluator;
import cdc.util.function.Visitor;
import cdc.util.lang.Checks;
import cdc.util.lang.FailureReaction;
/**
* Set of simple algorithms for slim graphs.
*
* In a slim graph, the node type can be any object and there is no edge structure.
* However, it is possible to explore neighbor nodes of any node.
*
* The following algorithms are provided.
*
* - Transitive closure of a node.
*
- Traversal (depth first and breadth first) starting on a node.
*
- Topological sort of the transitive closure of a node.
*
- Are 2 nodes connected?
*
*
* Static and non-static versions of functions are provided.
*
* @author Damien Carbonne
* @param The node type.
*/
public final class SlimGraph {
private static final Logger LOGGER = LogManager.getLogger(SlimGraph.class);
private static final String METHOD = "method";
private static final String NODE = "node";
private static final String ORDER = "order";
private static final String VISITOR = "visitor";
private static final String NEIGHBORS = "neighbors";
private final Function> neighbors;
/**
* Creates a slim graph.
*
* @param neighbors The function that returns the neighbors of a node.
*/
public SlimGraph(Function> neighbors) {
Checks.isNotNull(neighbors, NEIGHBORS);
this.neighbors = neighbors;
}
/**
* Created a slim graph from a graph adapter.
*
* @param adapter The graph adapter.
* @param direction The edge direction.
*/
public SlimGraph(GraphAdapter adapter,
EdgeDirection direction) {
Checks.isNotNull(adapter, "adapter");
this.neighbors = n -> adapter.getConnectedNodes(n, direction);
}
/**
* Computes the transitive closure of a node.
*
* @param node The starting node.
* @return The set of nodes that are directly or indirectly connected to {@code node}.
*/
public Set computeTransitiveClosure(N node) {
return computeTransitiveClosure(node, neighbors);
}
/**
* Most generic traversal function.
*
* @param node The initial node.
* @param method The traversal method.
* @param order The traversal order.
* @param visitor Visitor that will be invoked for each visited node.
* @param evaluator An optional evaluator used to control traversal termination.
* @throws IllegalArgumentException When {@code node}, {@code method}, {@code order},
* or {@code visitor} is {@code null}.
*/
public void traverse(N node,
TraversalMethod method,
TraversalOrder order,
Visitor visitor,
Evaluator evaluator) {
traverse(node, method, order, visitor, evaluator, neighbors);
}
/**
* Depth first traversal.
*
* @param node The initial node.
* @param order The traversal order.
* @param visitor Visitor that will be invoked for each visited node.
* @param evaluator An optional evaluator used to control traversal termination.
* @throws IllegalArgumentException When {@code node}, {@code order}, {@code visitor}
* or {@code neighbors} is {@code null}.
*/
public void traverseDepthFirst(N node,
TraversalOrder order,
Visitor visitor,
Evaluator evaluator) {
traverseDepthFirst(node, order, visitor, evaluator, neighbors);
}
/**
* Depth first pre order traversal.
*
* @param node The initial node.
* @param visitor Visitor that will be invoked for each visited node.
* @param evaluator An optional evaluator used to control traversal termination.
* @throws IllegalArgumentException When {@code node}, {@code visitor}
* or {@code neighbors} is {@code null}.
*/
public void traverseDepthFirstPre(N node,
Visitor visitor,
Evaluator evaluator) {
traverseDepthFirstPre(node, visitor, evaluator, neighbors);
}
/**
* Depth first post order traversal.
*
* @param node The initial node.
* @param visitor Visitor that will be invoked for each visited node.
* @param evaluator An optional evaluator used to control traversal termination.
* @throws IllegalArgumentException When {@code node}, {@code visitor}
* or {@code neighbors} is {@code null}.
*/
public void traverseDepthFirstPost(N node,
Visitor visitor,
Evaluator evaluator) {
traverseDepthFirstPost(node, visitor, evaluator, neighbors);
}
/**
* Breadth first traversal.
*
* @param node The initial node.
* @param order The traversal order.
* @param visitor Visitor that will be invoked for each visited node.
* @param evaluator An optional evaluator used to control traversal termination.
* @throws IllegalArgumentException When {@code node}, {@code order}, {@code visitor}
* or {@code neighbors} is {@code null}.
*/
public void traverseBreadthFirst(N node,
TraversalOrder order,
Visitor visitor,
Evaluator evaluator) {
traverseBreadthFirst(node, order, visitor, evaluator, neighbors);
}
/**
* Breadth first pre order traversal.
*
* @param node The initial node.
* @param visitor Visitor that will be invoked for each visited node.
* @param evaluator An optional evaluator used to control traversal termination.
* @throws IllegalArgumentException When {@code node}, {@code visitor}
* or {@code neighbors} is {@code null}.
*/
public void traverseBreadthFirstPre(N node,
Visitor visitor,
Evaluator evaluator) {
traverseBreadthFirstPre(node, visitor, evaluator, neighbors);
}
/**
* Breadth first post order traversal.
*
* @param node The initial node.
* @param visitor Visitor that will be invoked for each visited node.
* @param evaluator An optional evaluator used to control traversal termination.
* @throws IllegalArgumentException When {@code node}, {@code visitor}
* or {@code neighbors} is {@code null}.
*/
public void traverseBreadthFirstPost(N node,
Visitor visitor,
Evaluator evaluator) {
traverseBreadthFirstPost(node, visitor, evaluator, neighbors);
}
/**
* Returns a topological sort of nodes connected to a root node.
*
* If node {@code A} is connected to node {@code B}, then {@code A} will come before {@code B} in result.
*
* @param node The root node.
* @param reaction The reaction to adopt if the graph contains loops.
* @return A topological sort of nodes connected to {@code node} or an empty list.
* @throws IllegalArgumentException When {@code node} or {@code neighbors} is {@code null},
* or when there is a loop and {@code reaction} is {@link FailureReaction#FAIL}.
*/
public List topologicalSort(N node,
FailureReaction reaction) {
return topologicalSort(node, reaction, neighbors);
}
/**
* Returns a topological sort of nodes connected to a root node.
*
* If node {@code A} is connected to node {@code B}, then {@code A} will come before {@code B} in result.
*
* @param node The root node.
* @return A topological sort of nodes connected to {@code node} or an empty list.
* @throws IllegalArgumentException When {@code node} or {@code neighbors} is {@code null},
* or when there is a loop.
*/
public List topologicalSort(N node) {
return topologicalSort(node, neighbors);
}
/**
* Returns {@code true} when 2 nodes are connected.
*
* @param source The source node.
* @param target The target node.
* @return {@code true} when {@code source} is connected to {@code target}.
*/
public boolean areConnected(N source,
N target) {
return areConnected(source, target, neighbors);
}
/**
* Returns whether a particular node is member of a cycle or not.
*
* @param node The tested node.
* @return whether {@code node} is member of a cycle or not.
*/
public boolean nodeIsCycleMember(N node) {
return nodeIsCycleMember(node, neighbors);
}
/**
* Computes the transitive closure of a node.
*
* @param The node type.
* @param node The starting node.
* @param neighbors The function that returns the set of nodes that are directly connected to a node.
* @return The set of nodes that are directly or indirectly connected to {@code node}.
*/
public static Set computeTransitiveClosure(N node,
Function> neighbors) {
Checks.isNotNull(node, NODE);
Checks.isNotNull(neighbors, NEIGHBORS);
final Set done = new HashSet<>();
final Set todo = new HashSet<>();
todo.add(node);
while (!todo.isEmpty()) {
// Extract next node to process
final N next = todo.iterator().next();
todo.remove(next);
// next was not already processed
if (!done.contains(next)) {
done.add(next);
for (final N n : neighbors.apply(next)) {
if (!done.contains(n)) {
todo.add(n);
}
}
}
}
return done;
}
/**
* Most generic traversal function.
*
* @param The node type.
* @param node The initial node.
* @param method The traversal method.
* @param order The traversal order.
* @param visitor Visitor that will be invoked for each visited node.
* @param evaluator An optional evaluator used to control traversal termination.
* @param neighbors The function that returns the set of nodes that are directly connected to a node.
* @throws IllegalArgumentException When {@code node}, {@code method}, {@code order}, {@code visitor}
* or {@code neighbors} is {@code null}.
*/
public static void traverse(N node,
TraversalMethod method,
TraversalOrder order,
Visitor visitor,
Evaluator evaluator,
Function> neighbors) {
Checks.isNotNull(method, METHOD);
if (method == TraversalMethod.BREADTH_FIRST) {
traverseBreadthFirst(node, order, visitor, evaluator, neighbors);
} else {
traverseDepthFirst(node, order, visitor, evaluator, neighbors);
}
}
/**
* Depth first traversal.
*
* @param The node type.
* @param node The initial node.
* @param order The traversal order.
* @param visitor Visitor that will be invoked for each visited node.
* @param evaluator An optional evaluator used to control traversal termination.
* @param neighbors The function that returns the set of nodes that are directly connected to a node.
* @throws IllegalArgumentException When {@code node}, {@code order}, {@code visitor}
* or {@code neighbors} is {@code null}.
*/
public static void traverseDepthFirst(N node,
TraversalOrder order,
Visitor visitor,
Evaluator evaluator,
Function> neighbors) {
Checks.isNotNull(order, ORDER);
if (order == TraversalOrder.POST_ORDER) {
traverseDepthFirstPost(node, visitor, evaluator, neighbors);
} else {
traverseDepthFirstPre(node, visitor, evaluator, neighbors);
}
}
/**
* Depth first pre order traversal.
*
* @param The node type.
* @param node The initial node.
* @param visitor Visitor that will be invoked for each visited node.
* @param evaluator An optional evaluator used to control traversal termination.
* @param neighbors The function that returns the set of nodes that are directly connected to a node.
* @throws IllegalArgumentException When {@code node}, {@code visitor}
* or {@code neighbors} is {@code null}.
*/
public static void traverseDepthFirstPre(N node,
Visitor visitor,
Evaluator evaluator,
Function> neighbors) {
Checks.isNotNull(node, NODE);
Checks.isNotNull(visitor, VISITOR);
Checks.isNotNull(neighbors, NEIGHBORS);
final DepthFirstTraverser traverser = new DepthFirstTraverser<>(visitor,
evaluator,
neighbors);
traverser.traversePre(node);
}
/**
* Depth first post order traversal.
*
* @param The node type.
* @param node The initial node.
* @param visitor Visitor that will be invoked for each visited node.
* @param evaluator An optional evaluator used to control traversal termination.
* @param neighbors The function that returns the set of nodes that are directly connected to a node.
* @throws IllegalArgumentException When {@code node}, {@code visitor}
* or {@code neighbors} is {@code null}.
*/
public static void traverseDepthFirstPost(N node,
Visitor visitor,
Evaluator evaluator,
Function> neighbors) {
Checks.isNotNull(node, NODE);
Checks.isNotNull(visitor, VISITOR);
Checks.isNotNull(neighbors, NEIGHBORS);
final DepthFirstTraverser traverser = new DepthFirstTraverser<>(visitor,
evaluator,
neighbors);
traverser.traversePost(node);
}
/**
* Breadth first traversal.
*
* @param The node type.
* @param node The initial node.
* @param order The traversal order.
* @param visitor Visitor that will be invoked for each visited node.
* @param evaluator An optional evaluator used to control traversal termination.
* @param neighbors The function that returns the set of nodes that are directly connected to a node.
* @throws IllegalArgumentException When {@code node}, {@code order}, {@code visitor}
* or {@code neighbors} is {@code null}.
*/
public static void traverseBreadthFirst(N node,
TraversalOrder order,
Visitor visitor,
Evaluator evaluator,
Function> neighbors) {
Checks.isNotNull(order, ORDER);
if (order == TraversalOrder.POST_ORDER) {
traverseBreadthFirstPost(node, visitor, evaluator, neighbors);
} else {
traverseBreadthFirstPre(node, visitor, evaluator, neighbors);
}
}
/**
* Breadth first pre order traversal.
*
* @param The node type.
* @param node The initial node.
* @param visitor Visitor that will be invoked for each visited node.
* @param evaluator An optional evaluator used to control traversal termination.
* @param neighbors The function that returns the set of nodes that are directly connected to a node.
* @throws IllegalArgumentException When {@code node}, {@code visitor}
* or {@code neighbors} is {@code null}.
*/
public static void traverseBreadthFirstPre(N node,
Visitor visitor,
Evaluator evaluator,
Function> neighbors) {
Checks.isNotNull(node, NODE);
Checks.isNotNull(visitor, VISITOR);
Checks.isNotNull(neighbors, NEIGHBORS);
final BreadthFirstTraverser traverser =
new BreadthFirstTraverser<>(evaluator,
neighbors);
traverser.traversePre(node, visitor);
}
/**
* Breadth first post order traversal.
*
* @param The node type.
* @param node The initial node.
* @param visitor Visitor that will be invoked for each visited node.
* @param evaluator An optional evaluator used to control traversal termination.
* @param neighbors The function that returns the set of nodes that are directly connected to a node.
* @throws IllegalArgumentException When {@code node}, {@code visitor}
* or {@code neighbors} is {@code null}.
*/
public static void traverseBreadthFirstPost(N node,
Visitor visitor,
Evaluator evaluator,
Function> neighbors) {
Checks.isNotNull(node, NODE);
Checks.isNotNull(visitor, VISITOR);
Checks.isNotNull(neighbors, NEIGHBORS);
final BreadthFirstTraverser traverser =
new BreadthFirstTraverser<>(evaluator,
neighbors);
traverser.traversePost(node, visitor);
}
/**
* Returns a topological sort of nodes connected to a root node.
*
* If node {@code A} is connected to node {@code B}, then {@code A} will come before {@code B} in result.
*
* @param The node type.
* @param node The root node.
* @param reaction The reaction to adopt if the graph contains loops.
* @param neighbors The function that returns the set of nodes that are directly connected to a node.
* @return A topological sort of nodes connected to {@code node} or an empty list.
* @throws IllegalArgumentException When {@code node} or {@code neighbors} is {@code null},
* or when there is a loop and {@code reaction} is {@link FailureReaction#FAIL}.
*/
public static List topologicalSort(N node,
FailureReaction reaction,
Function> neighbors) {
Checks.isNotNull(node, NODE);
Checks.isNotNull(neighbors, NEIGHBORS);
final TopologicalSorter sorter = new TopologicalSorter<>(neighbors, node);
if (sorter.hasLoop) {
return FailureReaction.onError("Graph contains loops",
LOGGER,
reaction,
Collections.emptyList(),
IllegalArgumentException::new);
} else {
return sorter.sorted;
}
}
/**
* Returns a topological sort of nodes connected to a root node.
*
* If node {@code A} is connected to node {@code B}, then {@code A} will come before {@code B} in result.
*
* @param The node type.
* @param node The root node.
* @param neighbors The function that returns the set of nodes that are directly connected to a node.
* @return A topological sort of nodes connected to {@code node} or an empty list.
* @throws IllegalArgumentException When {@code node} or {@code neighbors} is {@code null},
* or when there is a loop.
*/
public static List topologicalSort(N node,
Function> neighbors) {
return topologicalSort(node, FailureReaction.FAIL, neighbors);
}
/**
* Returns {@code true} when 2 nodes are connected.
*
* @param source The source node.
* @param target The target node.
* @param neighbors The function that returns the set of nodes that are directly connected to a node.
* @return {@code true} when {@code source} is connected to {@code target}.
*/
public static boolean areConnected(N source,
N target,
Function> neighbors) {
final ConnectionDetector detector = new ConnectionDetector<>(neighbors, source, target);
return detector.areConnected();
}
/**
* Returns whether a particular node is member of a cycle or not.
*
* @param node The tested node.
* @param neighbors The function that returns the set of nodes that are directly connected to a node.
* @return whether {@code node} is member of a cycle or not.
*/
public static boolean nodeIsCycleMember(N node,
Function> neighbors) {
return areConnected(node, node, neighbors);
}
/**
* Internal class used to compute depth first traversal.
*
* @author Damien Carbonne
*
* @param The node type.
*/
private static class DepthFirstTraverser {
private final Visitor visitor;
private final Evaluator evaluator;
private final Function> neighbors;
private final Set visited = new HashSet<>();
public DepthFirstTraverser(Visitor visitor,
Evaluator evaluator,
Function> neighbors) {
this.visitor = visitor;
this.evaluator = evaluator;
this.neighbors = neighbors;
}
public void traversePre(N node) {
visitor.visit(node);
if (Evaluator.continueTraversal(evaluator, node)) {
visited.add(node);
for (final N succ : neighbors.apply(node)) {
if (!visited.contains(succ)) {
traversePre(succ);
}
}
}
}
void traversePost(N node) {
if (Evaluator.continueTraversal(evaluator, node)) {
visited.add(node);
for (final N succ : neighbors.apply(node)) {
if (!visited.contains(succ)) {
traversePost(succ);
}
}
}
visitor.visit(node);
}
}
/**
* Internal class used to compute breadth first traversal.
*
* @author Damien Carbonne
*
* @param The node type.
*/
private static class BreadthFirstTraverser {
private final Evaluator evaluator;
private final Function> neighbors;
public BreadthFirstTraverser(Evaluator evaluator,
Function> neighbors) {
this.evaluator = evaluator;
this.neighbors = neighbors;
}
public void traversePre(N from,
Visitor visitor) {
final Deque q = new ArrayDeque<>();
final Set visited = new HashSet<>();
q.addLast(from);
visited.add(from);
while (!q.isEmpty()) {
final N node = q.pollFirst();
visitor.visit(node);
if (Evaluator.continueTraversal(evaluator, node)) {
for (final N y : neighbors.apply(node)) {
if (!visited.contains(y)) {
visited.add(y);
q.addLast(y);
}
}
}
}
}
public void traversePost(N from,
Visitor visitor) {
final List visited = new ArrayList<>();
traversePre(from, visited::add);
for (int index = visited.size() - 1; index >= 0; index--) {
final N node = visited.get(index);
visitor.visit(node);
}
}
}
/**
* Internal class used to compute topological sort.
*
* @author Damien Carbonne
*
* @param The node type.
*/
private static class TopologicalSorter {
private final Function> neighbors;
private final Set visited = new HashSet<>();
private final Set visitedInThisCallStack = new HashSet<>();
final List sorted = new ArrayList<>();
boolean hasLoop = false;
public TopologicalSorter(Function> neighbors,
N from) {
this.neighbors = neighbors;
visit(from);
Collections.reverse(sorted);
}
private void visit(N node) {
if (visited.contains(node)) {
if (visitedInThisCallStack.contains(node)) {
hasLoop = true;
}
} else {
visited.add(node);
visitedInThisCallStack.add(node);
for (final N next : neighbors.apply(node)) {
visit(next);
}
visitedInThisCallStack.remove(node);
sorted.add(node);
}
}
}
/**
* Internal class used to detect connected nodes.
*
* @author Damien Carbonne
*
* @param The node type.
*/
private static class ConnectionDetector {
boolean found = false;
// Used to skip visit of source node
private boolean passedFirst = false;
public ConnectionDetector(Function> neighbors,
N source,
N target) {
final Evaluator evaluator = n -> {
if (n == target && passedFirst) {
found = true;
passedFirst = true;
return Evaluation.PRUNE;
} else {
passedFirst = true;
return Evaluation.CONTINUE;
}
};
traverseDepthFirstPre(source,
Visitor.ignore(),
evaluator,
neighbors);
}
public final boolean areConnected() {
return found;
}
}
}