/* * Copyright (C) 2012 Jason Gedge <http://www.gedge.ca> * * This file is part of the OpGraph project. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package ca.gedge.opgraph.dag; import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.WeakHashMap; /** * A generic implementation of a directed acyclic graph (DAG). Topological * ordering is enforced on the vertices of this graph (see * <a href="http://en.wikipedia.org/wiki/Topological_sorting">Wikipedia Entry</a>). * * @param <V> the vertex type, which implements {@link Vertex} * @param <E> the edge type, which implements {@link DirectedEdge} */ public class DirectedAcyclicGraph<V extends Vertex, E extends DirectedEdge<V>> implements Iterable<V> { /** The vertices in this DAG */ private ArrayList<V> vertices; /** The edges in this DAG */ private TreeSet<E> edges; /** * A mapping from vertex to its level. * * @see #getLevel(Object) */ private WeakHashMap<V, Integer> vertexLevels; /** A cache of incoming edges */ private WeakHashMap<V, SoftReference<Set<E>>> incomingEdgesCache; /** A cache of outgoing edges */ private WeakHashMap<V, SoftReference<Set<E>>> outgoingEdgesCache; /** Whether or not the topological sorting needs to be performed */ private boolean shouldSort; /** * Default constructor. */ public DirectedAcyclicGraph() { this.vertices = new ArrayList<V>(); this.edges = new TreeSet<E>(); this.vertexLevels = new WeakHashMap<V, Integer>(); this.incomingEdgesCache = new WeakHashMap<V, SoftReference<Set<E>>>(); this.outgoingEdgesCache = new WeakHashMap<V, SoftReference<Set<E>>>(); this.shouldSort = false; } /** * Adds a vertex to this DAG. * * @param vertex the vertex to add */ public void add(V vertex) { if(!vertices.contains(vertex)) { vertices.add(vertex); shouldSort = true; } } /** * Removes a vertex from this DAG. Any {@link DirectedEdge}s in this DAG that * reference this vertex will also be removed. * * @param vertex the vertex to remove * * @return <code>true</code> if this graph contained the given vertex, * <code>false</code> otherwise */ public boolean remove(V vertex) { final boolean removed = vertices.remove(vertex); if(removed) { shouldSort = true; // Remove edges which reference this vertex final ArrayList<E> edgesCopy = new ArrayList<E>(edges); for(E edge : edgesCopy) { if(edge.getSource() == vertex || edge.getDestination() == vertex) remove(edge); } } return removed; } /** * Gets whether or not this graph contains a specified vertex. * * @param vertex the vertex * * @return <code>true</code> if this graph contains the specified vertex, * <code>false</code> otherwise */ public boolean contains(V vertex) { return vertices.contains(vertex); } /** * Gets whether or not this graph contains a specified edge. * * @param edge the edge * * @return <code>true</code> if this graph contains the specified edge, * <code>false</code> otherwise */ public boolean contains(E edge) { return edges.contains(edge); } /** * Adds an edge to this DAG. * * @param edge the edge to add * * @throws VertexNotFoundException if <code>edge</code> contains vertices * that are not contained within this graph. * * @throws CycleDetectedException if adding <code>edge</code> will induce a cycle */ public void add(E edge) throws VertexNotFoundException, CycleDetectedException { if(!vertices.contains(edge.getSource())) throw new VertexNotFoundException(edge.getSource()); if(!vertices.contains(edge.getDestination())) throw new VertexNotFoundException(edge.getDestination()); edges.add(edge); // Clear out appropriate cache entries outgoingEdgesCache.remove(edge.getSource()); incomingEdgesCache.remove(edge.getDestination()); // Check if adding this edge created a cycle, and if so, remove it boolean oldShouldSort = shouldSort; shouldSort = true; if(!topologicalSort()) { edges.remove(edge); shouldSort = oldShouldSort; throw new CycleDetectedException("adding edge creates a cycle"); } } /** * Gets whether or not an edge can be added to this graph without raising * any exception defined in {@link #add(DirectedEdge)}. * * @param edge the edge to check * * @return <code>true</code> if the edge can be added without inducing a * cycle, <code>false</code> otherwise */ public boolean canAddEdge(E edge) { boolean canAdd = false; if(vertices.contains(edge.getSource()) && vertices.contains(edge.getDestination())) { try { edges.add(edge); // XXX It'd be nice to not have to do this, but instead either directly // add this edge to the cached values of each, or maybe pass this // edge to toplogicalSort directly outgoingEdgesCache.remove(edge.getSource()); incomingEdgesCache.remove(edge.getDestination()); // Check if adding this edge created a cycle, and if so, remove it boolean oldShouldSort = shouldSort; shouldSort = true; canAdd = topologicalSort(); shouldSort = oldShouldSort; } finally { outgoingEdgesCache.remove(edge.getSource()); incomingEdgesCache.remove(edge.getDestination()); edges.remove(edge); } } return canAdd; } /** * Removes an edge from this DAG. * * @param edge the edge to remove * * @return <code>true</code> if this graph contained the given vertex, * <code>false</code> otherwise */ public boolean remove(E edge) { final int initalSize = edges.size(); edges.remove(edge); final boolean removed = initalSize != edges.size(); if(removed) { // Clear out appropriate cache entries outgoingEdgesCache.remove(edge.getSource()); incomingEdgesCache.remove(edge.getDestination()); shouldSort = true; } return removed; } /** * Gets the set of vertices in this DAG. The list of vertices will be * ordered according to their topological ordering. * * @return An immutable {@link Set} of vertices. */ public List<V> getVertices() { topologicalSort(); return Collections.unmodifiableList(vertices); } /** * Gets the set of edges in this DAG. * * @return An immutable {@link Set} of edges. */ public Set<E> getEdges() { return Collections.unmodifiableSet(edges); } /** * Gets the level of a vertex. The level of a vertex <code>v</code> is * defined as: * <ul> * <li>0, if <code>getIncomingEdges(v) == 0</code></li> * <li><code>min(level of u) for u in getIncomingEdges(v).getSource()</code></li> * </ul> * * @param vertex the vertex * * @return the level of the vertex, or -1 if the vertex is not in this graph */ public int getLevel(V vertex) { if(!vertices.contains(vertex)) return -1; topologicalSort(); int ret = -1; if(vertexLevels.get(vertex) != null) ret = vertexLevels.get(vertex); return ret; } /** * Gets the incoming {@link DirectedEdge}s for a {@link Vertex}. * * @param vertex the vertex * * @return a {@link Set} of {@link DirectedEdge}s in this graph whose destination * is <code>vertex</code> */ public Set<E> getIncomingEdges(V vertex) { if(!vertices.contains(vertex)) return new TreeSet<E>(); // If not in cache, compute if(!incomingEdgesCache.containsKey(vertex)) { final TreeSet<E> cachedValue = new TreeSet<E>(); for(E edge : edges) { if(edge.getDestination() == vertex) cachedValue.add(edge); } incomingEdgesCache.put(vertex, new SoftReference<Set<E>>(cachedValue)); } return new TreeSet<E>(incomingEdgesCache.get(vertex).get()); } /** * Gets the outgoing {@link DirectedEdge}s for a {@link Vertex}. * * @param vertex the vertex * * @return a {@link Set} of {@link DirectedEdge}s in this graph whose source is * the <code>vertex</code> */ public Set<E> getOutgoingEdges(V vertex) { if(!vertices.contains(vertex)) return new TreeSet<E>(); // See if exists in cache if(!outgoingEdgesCache.containsKey(vertex)) { final TreeSet<E> cachedValue = new TreeSet<E>(); for(E edge : edges) { if(edge.getSource() == vertex) cachedValue.add(edge); } outgoingEdgesCache.put(vertex, new SoftReference<Set<E>>(cachedValue)); } return new TreeSet<E>(outgoingEdgesCache.get(vertex).get()); } @Override public Iterator<V> iterator() { topologicalSort(); return new Iterator<V>() { private Iterator<V> iter = vertices.iterator(); @Override public boolean hasNext() { return iter.hasNext(); } @Override public V next() { return iter.next(); } @Override public void remove() { // TODO perhaps allow removal? throw new UnsupportedOperationException("Removal via iterator not supported in DAGs"); } }; } /** * Topologically orders the vertices in this DAG. A topological ordering * is an ordering of a DAG's vertices such that for any edge * <tt>{u, v}</tt>, the vertex <tt>u</tt> comes before the vertex * <tt>v</tt> in the ordering. * * @return <code>true</code> if sorting was successful, <code>false</code> * otherwise (because a cycle exists). * * @see <a href="http://en.wikipedia.org/wiki/Topological_sorting">Wikipedia Article</a> */ private boolean topologicalSort() { boolean ret = true; if(shouldSort && vertices.size() == 1) { vertexLevels.put(vertices.iterator().next(), 0); } else if(shouldSort && vertices.size() > 1) { final ArrayList<V> orderedVertices = new ArrayList<V>(); final WeakHashMap<V, Integer> newLevels = new WeakHashMap<V, Integer>(); final HashMap<V, Integer> incomingEdgeCount = new HashMap<V, Integer>(); // for(V vertex : vertices) incomingEdgeCount.put(vertex, 0); // Gather initial incoming edge count for(E edge : edges) { int count = incomingEdgeCount.get(edge.getDestination()); incomingEdgeCount.put(edge.getDestination(), count + 1); } // Ordering for(int level = 0; orderedVertices.size() < vertices.size(); ++level) { // Find a vertex with zero incoming edges ArrayList<V> verticesToProcess = new ArrayList<V>(); for(Map.Entry<V, Integer> entry : incomingEdgeCount.entrySet()) { if(entry.getValue() == 0) verticesToProcess.add(entry.getKey()); } if(verticesToProcess.size() == 0) break; for(V vertex : verticesToProcess) { // Prevent reuse of this vertex orderedVertices.add(vertex); newLevels.put(vertex, level); incomingEdgeCount.put(vertex, -1); // Reduce incoming edge count after removing vertex for(E edge : getOutgoingEdges(vertex)) { V out = edge.getDestination(); incomingEdgeCount.put(out, incomingEdgeCount.get(out) - 1); } } } boolean cycleExists = false; for(Integer value : incomingEdgeCount.values()) { if(value > 0) { cycleExists = true; break; } } // If no cycle, we want to update the vertices to the new // ordered list and flag them as not needing sorting. if(cycleExists) { ret = false; } else { vertexLevels = newLevels; vertices = orderedVertices; shouldSort = false; } } return ret; } }