我正在寻找一种更有效的方法来修剪jgrapht中构造的有向无环图(DAG)。
DAG代表一组网络会话之间的关系。对话的父母是在孩子对话开始之前完成的任何对话。构建DAG相对简单,但存在许多不必要的关系。为了提高效率,我想修剪DAG,这样每个孩子都与父母的最小数量有直接的关系(或者相反,每个父母都有最少数量的直系孩子)。
我现在使用的剪枝实现(如下所示)基于streme中的代码。它适用于我所有手动构建的单元测试场景。但是,在实际数据集中,它通常相当慢。今天我遇到了215个顶点但超过22,000个边缘的场景。修剪DAG在服务器级硬件上花了将近8分钟的时钟时间 - 这对于我的直接用例是可以容忍的,但是对于更大的场景来说太慢了。
我认为我的问题类似于What algorithm can I apply to this DAG?和Algorithm for Finding Redundant Edges in a Graph or Tree中描述的问题。也就是说,我需要为我的DAG找到 transitive reduction 或最小代表。 jgrapht似乎不包含DAG的传递减少的直接实现,只有传递性闭包。
我正在寻找有关如何提高下面的实现效率的建议,或者可能是指向我可以使用的jgrapht的传递减少的现有实现的指针。
注意:或者,如果有一个不同的Java图形库包含传递减少的本机实现,我可以切换到该库。我对jgrapht的使用局限于一个200行的类,所以只要接口相似,交换它就不难。为了维护类接口(持久化到数据库),我需要一个DAG实现,它提供了一种获取给定节点的父节点和子节点的方法 - 类似于jgrapht' Graphs.predecessorListOf()
和{{1} }。
Graphs.successorListOf()
答案 0 :(得分:1)
优化实施
下面是我在第一篇评论中提到的带缓存的优化实现。
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jgrapht.Graphs;
import org.jgrapht.experimental.dag.DirectedAcyclicGraph;
import org.jgrapht.traverse.BreadthFirstIterator;
/**
* A class to compute transitive reduction for a jgrapht DAG.
* The basis for this implementation is streme (URL below), but I have made a variety of changes.
* It assumed that each vertex of type V has a toString() method which uniquely identifies it.
* @see <a href="https://code.google.com/p/streme/source/browse/streme/src/streme/lang/ast/analysis/ipda/DependencyGraphParallelizer.java">streme</a>
* @see <a href="http://en.wikipedia.org/wiki/Transitive_reduction">Transitive Reduction</a>
* @see <a href="http://en.wikipedia.org/wiki/Dijkstra's_algorithm">Dijkstra's Algorithm</a>
* @see <a href="http://en.wikipedia.org/wiki/Breadth-first_search">Breadth-First Search</a>
*/
public class TransitiveReduction {
/**
* Compute transitive reduction for a DAG.
* Each vertex is assumed to have a toString() method which uniquely identifies it.
* @param graph Graph to compute transitive reduction for
*/
public static <V, E> void prune(DirectedAcyclicGraph<V, E> graph) {
ConnectionCache<V, E> cache = new ConnectionCache<V, E>(graph);
Deque<V> deque = new ArrayDeque<V>(graph.vertexSet());
while (!deque.isEmpty()) {
V vertex = deque.pop();
prune(graph, vertex, cache);
}
}
/** Prune a particular vertex in a DAG, using the passed-in cache. */
private static <V, E> void prune(DirectedAcyclicGraph<V, E> graph, V vertex, ConnectionCache<V, E> cache) {
List<V> targets = Graphs.successorListOf(graph, vertex);
for (int i = 0; i < targets.size(); i++) {
for (int j = i + 1; j < targets.size(); j++) {
V child1 = targets.get(i);
V child2 = targets.get(j);
if (cache.isConnected(child1, child2)) {
E edge = graph.getEdge(vertex, child2);
graph.removeEdge(edge);
}
}
}
}
/** A cache that stores previously-computed connections between vertices. */
private static class ConnectionCache<V, E> {
private DirectedAcyclicGraph<V, E> graph;
private Map<String, Boolean> map;
public ConnectionCache(DirectedAcyclicGraph<V, E> graph) {
this.graph = graph;
this.map = new HashMap<String, Boolean>(graph.edgeSet().size());
}
public boolean isConnected(V startVertex, V endVertex) {
String key = startVertex.toString() + "-" + endVertex.toString();
if (!this.map.containsKey(key)) {
boolean connected = isConnected(this.graph, startVertex, endVertex);
this.map.put(key, connected);
}
return this.map.get(key);
}
private static <V, E> boolean isConnected(DirectedAcyclicGraph<V, E> graph, V startVertex, V endVertex) {
BreadthFirstIterator<V, E> iter = new BreadthFirstIterator<V, E>(graph, startVertex);
while (iter.hasNext()) {
V vertex = iter.next();
if (vertex.equals(endVertex)) {
return true;
}
}
return false;
}
}
}
<强>改进强>
在其他微小变化中,我通过添加缓存改进了streme实现,因此我们不需要重新计算之前看到的两个顶点之间的路径。我还改变了streme实现,使用BreadthFirstIterator
来检查节点之间的连接,而不是依赖于Dijkstra的算法。 Dijkstra的算法计算最短路径,但我们在这里关心的是是否存在任何路径。将检查短路使得这种实现比原始实现更有效。
其他潜在改进
对于大型DAG,此实现可能非常慢,特别是在平均顶点有很多子节点的情况下。这有两个原因:算法本身的效率,以及连接缓存的实现。该算法按 O(vc 2 b d )进行缩放,其中 v 是顶点数, c 是绑定到平均顶点的子节点数, b 是DAG在平均顶点处的宽度, d 是DAG的深度在平均顶点。缓存是一个简单的HashMap
,用于跟踪两个DAG顶点之间是否存在路径。与原始的非缓存实现相比,添加缓存使我的性能提高了14-20倍。但是,随着DAG变大,与缓存相关的开销有时会开始变得很大。
如果您仍然遇到性能问题,解决该问题的一种方法可能是逐步修剪DAG,而不是等到所有关系都被添加。根据DAG中的关系,这可以通过减少平均子节点数并最小化连接缓存所需的大小来提供帮助。在我最近的测试(4500个顶点)中,通过在添加每组10-15个顶点之后修剪DAG,我能够获得实质性的改进。随着该算法的其他改进,逐步修剪导致处理时间从4-6小时减少到约10分钟。
测试和验证
我对此进行了单元测试,我相信它能按预期工作,但我非常愿意研究算法的潜在问题。为此,我添加了专门针对cthiebaud场景的测试,以防万一我在其他测试中错过了一个角落案例。
以下是结果的可视化。左侧图形是原始图形,右侧图形是修剪后的图形。这些图片是通过从jgrapht的DOTExporter
渲染DOT输出生成的。
这是我所期望的,所以我仍然认为实施工作正常。
答案 1 :(得分:0)
我担心你的算法无法处理正确的图形,例如边缘(A,B),(B,C),(C,D)和(A,D),最后一个边缘(A,D)不是删除。在Michael Clerx的类似问题transitive reduction algorithm: pseudocode?的答案中,我在python中找到了一个正确的算法。我使用jgrapht https://github.com/aequologica/dagr/blob/develop/dagr-web/src/main/java/net/aequologica/neo/dagr/jgrapht/TransitiveReduction.java将他的python代码移植到java。
我只使用微小的图表,也许这个解决方案不适合大图。