All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ajjpj.afoundation.collection.graph.ADiGraph Maven / Gradle / Ivy

The newest version!
package com.ajjpj.afoundation.collection.graph;

import com.ajjpj.afoundation.collection.ACollectionHelper;
import com.ajjpj.afoundation.collection.immutable.AHashMap;
import com.ajjpj.afoundation.collection.immutable.AList;
import com.ajjpj.afoundation.collection.immutable.AMap;
import com.ajjpj.afoundation.function.APredicateNoThrow;

import java.io.Serializable;
import java.util.*;


/**
 * This class represents a directed graph - in the sense of a data structure, not of its visual representation. It is
 *  immutable, i.e. its nodes and edges are fixed on initialization. It is thread safe, i.e. it is safe to access concurrently
 *  from several threads.

* * A graph is represented as a collection of nodes and a second collection of edges. There is no requirement for the graph to be * connected.

* * This class strikes a balance between memory footprint and speed. It lazily computes and then caches results that are deemed * likely to be reused. * * @author arno */ public class ADiGraph> implements Serializable { private final Object LOCK = new Object (); private final Object[] nodes; private final AEdge[] edges; /** * This is a convenience factory method that extracts the list of nodes from the edges. It assumes that every node has at least one * edge going from or to it. */ public static > ADiGraph create (Collection edges) { final Set result = new HashSet<> (); for (E edge: edges) { result.add (edge.getFrom ()); result.add (edge.getTo ()); } return create (result, edges); } /** * This factory method creates a graph with the given nodes and edges. It expressly allows nodes that have no edges attached to them. */ public static > ADiGraph create (Collection nodes, Collection edges) { final Object[] nodeArr = new Object[nodes.size ()]; final AEdge[] edgeArr = new AEdge[edges.size ()]; int idx = 0; for (N node: nodes) { nodeArr[idx] = node; idx += 1; } idx = 0; for (E edge: edges) { edgeArr[idx] = edge; idx += 1; } return new ADiGraph (nodeArr, edgeArr); } private ADiGraph (Object[] nodes, AEdge[] edges) { this.nodes = nodes; this.edges = edges; } /** * @return this graph's nodes */ public Collection nodes () { return new ArrayIterable<> (nodes); } /** * @return this graph's edges */ public Collection edges () { return new ArrayIterable<> (edges); } //TODO helper: create list of edges from 'partial order' private transient AMap>> _incomingPathsInternal; private transient AMap>> _outgoingPathsInternal; private transient AList> _cyclesInternal; /** * This method does the reachability analysis in a way that is useful for many other methods. */ private void initPathsInternal() { synchronized (LOCK) { if (_incomingPathsInternal == null) { AMap>> incomingPaths = AHashMap.empty (); //noinspection unchecked incomingPaths = incomingPaths.withDefaultValue (AList.nil); AMap>> outgoingPaths = AHashMap.empty (); //noinspection unchecked outgoingPaths = outgoingPaths.withDefaultValue (AList.nil); AList> cycles = AList.nil (); for (N curNode : nodes ()) { // iterate over nodes, treat 'curNode' as a target final Iterable curIncoming = incomingEdges (curNode); List> unfinishedBusiness = new ArrayList<> (); for (E incomingEdge : curIncoming) { unfinishedBusiness.add (AEdgePath.create (incomingEdge)); } AList> nonCycles = AList.nil (); while (unfinishedBusiness.size () > 0) { final List> curBusiness = unfinishedBusiness; for (AEdgePath p : unfinishedBusiness) { if (!p.hasCycle () || p.isMinimalCycle ()) nonCycles = nonCycles.cons (p); if (p.isMinimalCycle ()) cycles = cycles.cons (p); } unfinishedBusiness = new ArrayList<> (); for (AEdgePath curPath : curBusiness) { final Iterable l = incomingEdges (curPath.getFrom ()); for (E newEdge : l) { final AEdgePath pathCandidate = curPath.prepend (newEdge); if (!pathCandidate.hasNonMinimalCycle ()) { unfinishedBusiness.add (pathCandidate); } } } } incomingPaths = incomingPaths.updated (curNode, nonCycles); for (AEdgePath p : nonCycles) { outgoingPaths = outgoingPaths.updated (p.getFrom (), outgoingPaths.getRequired (p.getFrom ()).cons (p)); } } _incomingPathsInternal = incomingPaths; _outgoingPathsInternal = outgoingPaths; _cyclesInternal = cycles; } } } private transient Map> _incomingEdges = null; private Map> incomingEdges() { if (_incomingEdges == null) { _incomingEdges = new HashMap<> (); for (E edge: edges ()) { List edgeList = _incomingEdges.get (edge.getTo ()); if (edgeList == null) { edgeList = new ArrayList<> (); _incomingEdges.put (edge.getTo (), edgeList); } edgeList.add (edge); } } return _incomingEdges; } /** * @return all edges having the given node as their target. */ public Collection incomingEdges (N node) { final List result = incomingEdges ().get (node); return result != null ? Collections.unmodifiableList (result) : Collections.emptyList (); } private transient Map> _outgoingEdges = null; private Map> outgoingEdges() { if (_outgoingEdges == null) { _outgoingEdges = new HashMap<> (); for (E edge: edges ()) { List edgeList = _outgoingEdges.get (edge.getFrom ()); if (edgeList == null) { edgeList = new ArrayList<> (); _outgoingEdges.put (edge.getFrom (), edgeList); } edgeList.add (edge); } } return _outgoingEdges; } /** * @return all edges having the given node as their source. */ public Collection outgoingEdges (N node) { final List result = outgoingEdges ().get (node); return result != null ? Collections.unmodifiableList (result) : Collections.emptyList (); } /** * @return all paths having this node as their source - more specifically, all paths that are either acyclic or minimal cycles. */ public Collection> outgoingPaths (N node) { initPathsInternal (); return _outgoingPathsInternal.getRequired (node).asJavaUtilList (); } /** * @return all paths having this node as their target - more specifically, all paths that are either acyclic or minimal cycles. */ public Collection> incomingPaths (N node) { initPathsInternal (); return _incomingPathsInternal.getRequired (node).asJavaUtilList (); } /** * @return true if and only if there is an edge from {@code from} to {@code to}. */ public boolean hasEdge (N from, N to) { for (E edge: incomingEdges (to)) { if (from.equals (edge.getFrom ())) { return true; } } return false; } /** * @return true if and only if there is a path from {@code from} to {@code to}. */ public boolean hasPath (N from, N to) { for (AEdgePath path: incomingPaths (to)) { if (from.equals (path.getFrom ())) { return true; } } return false; } /** * A directed graph defines a partial order through 'reachability', and this method sorts the graph's nodes based on that * partial order. * * @return all nodes, sorted in such a way that a node is guaranteed to come before all nodes that can be reached from it. * @throws AGraphCircularityException if the graph has cycles and can therefore not be sorted by reachability */ public List sortedNodesByReachability() throws AGraphCircularityException { if (hasCycles()) { throw new AGraphCircularityException (); } final Object[] result = new Object[nodes.length]; int nextIdx = 0; final Set unprocessed = new HashSet<> (); for (Object node: nodes) { //noinspection unchecked unprocessed.add ((N) node); } //TODO Map with 'remaining' incoming edges, decrement when a node is 'processed' --> JMH while (! unprocessed.isEmpty ()) { final Set nextBatch = ACollectionHelper.filter (unprocessed, new APredicateNoThrow () { @Override public boolean apply (N n) { for (E e : incomingEdges (n)) { if (unprocessed.contains (e.getFrom ())) { return false; } } return true; } }); unprocessed.removeAll (nextBatch); for (N n: nextBatch) { result[nextIdx] = n; nextIdx += 1; } } return new ArrayIterable<> (result); } /** * @return all paths in the graph that are minimal cycles, i.e. paths with the same start and end point and in which every node appears exactly once as a start and end point. */ public Collection> minimalCycles () { initPathsInternal (); return _cyclesInternal.asJavaUtilCollection (); } /** * @return true if and only if the graph contains cycles */ public boolean hasCycles() { return ! minimalCycles ().isEmpty (); } /** * @return true if and only if the graph does not contain cycles */ public boolean isAcyclic() { return minimalCycles ().isEmpty (); } /** * @return true if and only if every node has at most one edge pointing at it, and there is exactly one root node. */ public boolean isTree() { return isForest () && rootNodes ().size() == 1; } private Boolean _isForest; /** * @return true if and only if every node has at most one edge pointing at it. */ public boolean isForest() { synchronized (LOCK) { if (_isForest == null) { _isForest = !hasCycles () && ACollectionHelper.forAll (nodes (), new APredicateNoThrow () { @Override public boolean apply (N n) { return incomingEdges (n).size () <= 1; // no node with more than one edge pointing to it } }); } } return _isForest; } private transient Collection _rootNodes; /** * @return all nodes with no edges pointing at them. */ public Collection rootNodes() { synchronized (LOCK) { if (_rootNodes == null) { _rootNodes = ACollectionHelper.filter (nodes (), new APredicateNoThrow () { @Override public boolean apply (N n) { return incomingEdges (n).isEmpty (); } }); } return _rootNodes; } } private transient Collection _leafNodes; /** * @return all nodes with no edge pointing from them. */ public Collection leafNodes() { synchronized (this) { if (_leafNodes == null) { _leafNodes = ACollectionHelper.filter (nodes (), new APredicateNoThrow () { @Override public boolean apply (N n) { return outgoingEdges (n).isEmpty (); } }); } return _leafNodes; } } private static class ArrayIterable extends AbstractList { private final Object[] data; ArrayIterable (Object[] data) { this.data = data; } @SuppressWarnings ("NullableProblems") @Override public Iterator iterator () { return new ArrayIterator<> (data); } @SuppressWarnings ("unchecked") @Override public T get (int index) { return (T) data[index]; } @Override public int size () { return data.length; } } private static class ArrayIterator implements Iterator { private final Object[] data; private int nextIdx = 0; public ArrayIterator (Object[] data) { this.data = data; } @Override public boolean hasNext () { return nextIdx < data.length; } @SuppressWarnings ("unchecked") @Override public T next () { final T result = (T) data[nextIdx]; nextIdx += 1; return result; } @Override public void remove () { throw new UnsupportedOperationException (); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy