/**
* Copyright 2011-2017 Asakusa Framework Team.
*
* 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.asakusafw.utils.graph;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import com.asakusafw.utils.graph.Graph.Vertex;
/**
* Operations for {@link Graph}.
*/
public final class Graphs {
/**
* Creates a new {@link Graph} instance without any vertices.
* @param <V> the vertex value type
* @return the created instance
*/
public static <V> Graph<V> newInstance() {
return new HashGraph<>();
}
/**
* Returns a copy of the target graph.
* @param <V> the vertex value type
* @param graph the target graph
* @return the copy
* @throws IllegalArgumentException if the parameter is {@code null}
*/
public static <V> Graph<V> copy(Graph<? extends V> graph) {
if (graph == null) {
throw new IllegalArgumentException("graph must not be null"); //$NON-NLS-1$
}
Graph<V> copy = newInstance();
for (Graph.Vertex<? extends V> vertex : graph) {
copy.addEdges(vertex.getNode(), vertex.getConnected());
}
return copy;
}
/**
* Returns each head vertex in the graph, which has no preceding vertices.
* @param <V> the vertex value type
* @param graph the target graph
* @return the head vertices
* @throws IllegalArgumentException if the parameter is {@code null}
*/
public static <V> Set<V> collectHeads(Graph<? extends V> graph) {
if (graph == null) {
throw new IllegalArgumentException("graph must not be null"); //$NON-NLS-1$
}
Set<V> results = new HashSet<>(graph.getNodeSet());
for (Vertex<? extends V> vertex : graph) {
results.removeAll(vertex.getConnected());
}
return results;
}
/**
* Returns each tail vertex in the graph, which has no succeeding vertices.
* @param <V> the vertex value type
* @param graph the target graph
* @return the tail vertices
* @throws IllegalArgumentException if the parameter is {@code null}
*/
public static <V> Set<V> collectTails(Graph<? extends V> graph) {
if (graph == null) {
throw new IllegalArgumentException("graph must not be null"); //$NON-NLS-1$
}
Set<V> results = new HashSet<>();
for (Vertex<? extends V> vertex : graph) {
if (vertex.getConnected().isEmpty()) {
results.add(vertex.getNode());
}
}
return results;
}
/**
* Returns the transitively connected all vertices from the starting vertices.
* If a starting vertex is not connected to any vertices, the resulting set will not contain it even if it is a
* starting vertex.
* @param <V> the vertex value type
* @param graph the target graph
* @param startNodes the starting vertices
* @return the transitive connected vertices from the starting vertices
* @throws IllegalArgumentException if the parameters are {@code null}
*/
public static <V> Set<V> collectAllConnected(Graph<? extends V> graph, Collection<? extends V> startNodes) {
if (graph == null) {
throw new IllegalArgumentException("graph must not be null"); //$NON-NLS-1$
}
if (startNodes == null) {
throw new IllegalArgumentException("startNodes must not be null"); //$NON-NLS-1$
}
Set<V> connected = new HashSet<>();
for (V start : startNodes) {
findAllConnected(graph, start, connected);
}
return connected;
}
/**
* Returns the succeeding nearest vertices which match the condition, from the starting vertices.
* The nearest vertices must be a set of <em>nearest</em> which satisfies the following all conditions:
* <ul>
* <li>
* Is transitively connected from any starting vertices
* ({@link #collectAllConnected(Graph, Collection) collectAllConnected(graph, startNodes).contains(nearest)})
* </li>
* <li>
* Satisfies the {@code matcher} rule ({@link Predicate#test(Object) matcher.test(nearest)})
* </li>
* <li>
* Reachable from any starting vertices, NOT via other <em>nearest</em>
* </li>
* </ul>
* <p>
* The resulting set may not contain the starting vertices, excepts the starting vertices also satisfies above
* the conditions.
* @param <V> the vertex value type
* @param graph the target graph
* @param startNodes the starting vertices
* @param matcher the matcher
* @return the nearest vertices
* @throws IllegalArgumentException if the parameters are {@code null}
*/
public static <V> Set<V> findNearest(
Graph<? extends V> graph,
Collection<? extends V> startNodes,
Predicate<? super V> matcher) {
if (graph == null) {
throw new IllegalArgumentException("graph must not be null"); //$NON-NLS-1$
}
if (startNodes == null) {
throw new IllegalArgumentException("startNodes must not be null"); //$NON-NLS-1$
}
if (matcher == null) {
throw new IllegalArgumentException("matcher must not be null"); //$NON-NLS-1$
}
LinkedList<V> queue = new LinkedList<>();
for (V start : startNodes) {
queue.addAll(graph.getConnected(start));
}
Set<V> saw = new HashSet<>();
Set<V> results = new HashSet<>();
while (queue.isEmpty() == false) {
V first = queue.removeFirst();
if (saw.contains(first)) {
continue;
}
saw.add(first);
boolean matched = matcher.test(first);
if (matched) {
results.add(first);
} else {
queue.addAll(graph.getConnected(first));
}
}
return results;
}
/**
* Returns the succeeding nearest vertices which match the condition, from the starting vertices,
* and vertices on their routes. In other words, this method returns all vertices on the searching route on
* {@link #findNearest(Graph, Collection, Predicate)}. The resulting set may not contain the starting vertices,
* excepts the starting vertices also on their searching route.
* @param <V> the vertex value type
* @param graph the target graph
* @param startNodes the starting vertices
* @param matcher the vertices predicate
* @return the nearest vertices and vertices on their route
* @throws IllegalArgumentException if the parameters are {@code null}
*/
public static <V> Set<V> collectNearest(
Graph<? extends V> graph,
Collection<? extends V> startNodes,
Predicate<? super V> matcher) {
if (graph == null) {
throw new IllegalArgumentException("graph must not be null"); //$NON-NLS-1$
}
if (startNodes == null) {
throw new IllegalArgumentException("startNodes must not be null"); //$NON-NLS-1$
}
if (matcher == null) {
throw new IllegalArgumentException("matcher must not be null"); //$NON-NLS-1$
}
LinkedList<V> queue = new LinkedList<>();
for (V start : startNodes) {
queue.addAll(graph.getConnected(start));
}
Set<V> saw = new HashSet<>();
Set<V> results = new HashSet<>();
while (queue.isEmpty() == false) {
V first = queue.removeFirst();
if (saw.contains(first)) {
continue;
}
saw.add(first);
boolean matched = matcher.test(first);
if (matched) {
results.add(first);
} else {
results.add(first);
queue.addAll(graph.getConnected(first));
}
}
return results;
}
/**
* Returns the all cyclic sub-graphs in the target graph.
* @param <V> the vertex value type
* @param graph the target graph
* @return the sets of cyclic connected vertices
* @throws IllegalArgumentException if the parameter is {@code null}
*/
public static <V> Set<Set<V>> findCircuit(Graph<? extends V> graph) {
if (graph == null) {
throw new IllegalArgumentException("graph must not be null"); //$NON-NLS-1$
}
Set<Set<V>> results = new HashSet<>();
Set<Set<V>> sccs = Graphs.findStronglyConnectedComponents(graph);
for (Set<V> scc : sccs) {
if (scc.size() >= 2) {
// if SCC has > 2 vertices, it is cyclic
results.add(scc);
} else if (scc.size() == 1) {
// if SCC has = 1 vertex, it is cyclic only if the vertex has self edge
V vertex = scc.iterator().next();
if (graph.isConnected(vertex, vertex)) {
results.add(scc);
}
}
}
return results;
}
/**
* Returns the all strongly connected components in the target graph.
* @param <V> the vertex value type
* @param graph the target graph
* @return the strongly connected components
* @throws IllegalArgumentException if the parameter is {@code null}
*/
public static <V> Set<Set<V>> findStronglyConnectedComponents(Graph<? extends V> graph) {
if (graph == null) {
throw new IllegalArgumentException("graph must not be null"); //$NON-NLS-1$
}
List<? extends V> postorder = computePostOrderByDepth(graph);
Graph<? extends V> tgraph = transpose(graph);
List<Set<V>> results = new ArrayList<>();
Set<V> saw = new HashSet<>();
for (int i = postorder.size() - 1; i >= 0; --i) {
V start = postorder.get(i);
if (saw.contains(start)) {
continue;
}
saw.add(start);
Set<V> connected = new HashSet<>();
connected.add(start);
VisitFrame<V> top = VisitFrame.build(tgraph, start);
while (top != null) {
while (top.branches.hasNext()) {
V node = top.branches.next();
if (saw.contains(node)) {
continue;
}
saw.add(node);
connected.add(node);
top = top.push(node);
}
top = top.previous;
}
results.add(connected);
}
return new HashSet<>(results);
}
/**
* Sorts the vertices from the tail to head topologically, and returns their list.
* If the {@code graph} does not contain any cycles, the resulting result must satisfy the following:
<pre>{@code
for i = 0..list.size-1:
for j = i+1..list.size-1:
assert graph.isConnected(list[i], list[j]) == false;
}</pre>
* @param <V> the vertex value type
* @param graph the target graph
* @return the sorted vertices
* @throws IllegalArgumentException if the parameter is {@code null}
*/
public static <V> List<V> sortPostOrder(Graph<? extends V> graph) {
if (graph == null) {
throw new IllegalArgumentException("graph must not be null"); //$NON-NLS-1$
}
List<? extends V> postorder = computePostOrderByDepth(graph);
return new ArrayList<>(postorder);
}
/**
* Creates a new transposed graph from the target graph.
* The transposed graph <em>reverse</em> must satisfy the following:
<pre>{@code
graph.contains(a) <=> reverse.contains(a), and
graph.isConnected(a, b) <=> reverse.isConnected(b, a).
}</pre>
* @param <V> the vertex value type
* @param graph the target graph
* @return the created graph
* @throws IllegalArgumentException if the parameter is {@code null}
*/
public static <V> Graph<V> transpose(Graph<V> graph) {
if (graph == null) {
throw new IllegalArgumentException("graph must not be null"); //$NON-NLS-1$
}
Graph<V> results = new HashGraph<>();
for (Graph.Vertex<V> vertex : graph) {
V from = vertex.getNode();
results.addNode(from);
for (V to : vertex.getConnected()) {
results.addEdge(to, from);
}
}
return results;
}
/**
* Creates a new subgraph from the target graph.
* @param <V> the vertex value type
* @param graph the target graph
* @param matcher only test to the subgraph members
* @return the created subgraph
* @throws IllegalArgumentException if the parameters are {@code null}
*/
public static <V> Graph<V> subgraph(Graph<? extends V> graph, Predicate<? super V> matcher) {
if (graph == null) {
throw new IllegalArgumentException("graph must not be null"); //$NON-NLS-1$
}
if (matcher == null) {
throw new IllegalArgumentException("matcher must not be null"); //$NON-NLS-1$
}
Graph<V> subgraph = newInstance();
Map<V, Boolean> matchedSet = new HashMap<>();
for (V vertex : graph.getNodeSet()) {
boolean matched = matcher.test(vertex);
if (matched) {
subgraph.addNode(vertex);
}
matchedSet.put(vertex, matched);
}
if (subgraph.isEmpty()) {
return subgraph;
}
for (Graph.Vertex<? extends V> vertex : graph) {
V from = vertex.getNode();
assert matchedSet.containsKey(from);
if (Boolean.FALSE.equals(matchedSet.get(from))) {
continue;
}
for (V to : vertex.getConnected()) {
assert matchedSet.containsKey(to);
if (Boolean.FALSE.equals(matchedSet.get(to))) {
continue;
}
subgraph.addEdge(from, to);
}
}
return subgraph;
}
private static <V> List<V> computePostOrderByDepth(Graph<? extends V> graph) {
assert graph != null;
List<V> results = new ArrayList<>();
Set<V> saw = new HashSet<>();
for (Graph.Vertex<? extends V> start : graph) {
if (saw.contains(start.getNode())) {
continue;
}
saw.add(start.getNode());
VisitFrame<V> top = VisitFrame.build(graph, start.getNode());
while (top != null) {
while (top.branches.hasNext()) {
V node = top.branches.next();
if (saw.contains(node)) {
continue;
}
saw.add(node);
top = top.push(node);
}
results.add(top.node);
top = top.previous;
}
}
return results;
}
private static <V> void findAllConnected(Graph<? extends V> graph, V start, Set<V> connected) {
assert graph != null;
assert connected != null;
if (connected.contains(start)) {
return;
}
if (graph.contains(start) == false) {
return;
}
VisitFrame<V> top = VisitFrame.build(graph, start);
while (top != null) {
while (top.branches.hasNext()) {
V node = top.branches.next();
if (connected.contains(node)) {
continue;
}
connected.add(node);
top = top.push(node);
}
top = top.previous;
}
}
private Graphs() {
return;
}
private static final class VisitFrame<V> {
private final Graph<? extends V> graph;
final VisitFrame<V> previous;
final V node;
final Iterator<? extends V> branches;
VisitFrame(VisitFrame<V> previous, Graph<? extends V> graph, V node, Iterator<? extends V> branch) {
assert graph != null;
assert branch != null;
this.graph = graph;
this.previous = previous;
this.node = node;
this.branches = branch;
}
static <V> VisitFrame<V> build(Graph<? extends V> graph, V node) {
assert graph != null;
Iterator<? extends V> branch = graph.getConnected(node).iterator();
return new VisitFrame<>(null, graph, node, branch);
}
VisitFrame<V> push(V nextNode) {
Iterator<? extends V> nextBranch = graph.getConnected(nextNode).iterator();
return new VisitFrame<>(this, graph, nextNode, nextBranch);
}
}
}