Faster cycle detection in a Directed Acyclic Graph?
我有一个Ruby1.9.3中的程序,它构造了一个RubyTree。我的数据最好描述为一个有向无环图(DAG);请注意,它不是一个多树。好吧,至少数据应该是一个DAG,尽管用户尽最大努力用坏数据屏蔽我的程序。
我通过解析XML文档动态构建DAG。XML文档没有显式地指定树结构,但提供了整数ID的交叉引用,这些ID在文档中的元素之间建立链接。
我需要确保rubytree不包含任何循环。源数据可能(错误地)有一个循环,如果有,我的程序需要知道它,而不是进入无限循环或崩溃。为了实现这一点,我将Ruby标准库的tsort模块混合到RubyTree的
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | module Tree class TreeNode include TSort def tsort_each_node(&block) self.each(&block) end def tsort_each_child(node, &block) node.get_children().each { |child| yield child } end def add(child, at_index = -1) #The standard RubyTree implementation of add goes here begin self.tsort() rescue TSort::Cyclic => exce self.remove!(child) raise exce end return child end end end |
我也不得不修改其他一些方法。基本上,任何需要遍历树或子级的东西都需要执行tsort,或者摆脱它对遍历的依赖(例如,我简化了
现在,我的程序功能正常。我已经做了大量的测试,结果运行良好:我所要做的就是在我的代码中的正确点上拯救
问题是,在大小为75000左右的rubytree上,边数非常接近等于顶点数减去1,迭代运行tarjan的算法会产生一个看起来很二次的算法复杂性。Tarjan本身就是
但是太慢了!仅此一个循环就需要半个多小时,而我的程序由其他几个步骤组成,这些步骤会占用他们自己的合理时间。但从江户记1(10)的结果来看,仅仅表演塔扬的表演就占了表演的大部分。我尝试在rubytree
我发现这篇由Tarjan撰写的很棒的论文,他发明了Ruby的
我是在这里错过了一些愚蠢的东西,还是我最好把它放在脑力工作中分析Tarjan的论文,并尝试想出一个Ruby实现的算法?注意,我并不特别关心算法的拓扑排序方面;这是我真正想要的副作用。如果这棵树没有拓扑排序,但仍然保证没有循环,我会非常高兴的。
同样值得注意的是,在我的源数据中,周期是很少的。也就是说,循环可能由于数据输入过程中的手动错误而发生,但它们不会有意发生,并且应该始终向程序报告,以便它能够告诉我,这样我就可以用一个billcyclob来击败某人,因为他输入了错误的数据。另外,即使程序检测到一个特别讨厌的循环,它也必须继续运行下去,所以我不能把我的头埋在沙子里,希望不会有任何循环。
实际问题是什么?根据一些人的要求,这里有一个演示,您可以在工作中查看这个问题。
安装稳定版本的Rubytree(我使用MRI 1.9.3)。然后比较输出
红宝石的树脂宝石似乎解决了我最严重的问题。我以前试过Grater,但是它不能加载,因为它与Ruby1.9.3不兼容,但是Plexus是Grater的一个分支,与1.9.3一起工作。
我的问题是,我使用的数据结构(rubytree)不是为处理循环而设计的,但是Plexus有向图实际上可以继续处理循环。API的设计考虑到了它们。
我使用的解决方案非常简单:基本上,既然我的图形数据结构不依赖于循环,我可以在图形生成例程的末尾调用Tarjan的算法——实际上,有一个很好的包装器
不幸的是,现在我面临的挑战是开发一个最小反馈弧集问题(最小Fas问题)的实现,这是NP困难的。最小的fas问题是必需的,因为我需要删除图中最少的插入弧数,使其成为非循环的。
我现在的计划是从Plexus获取强连接组件列表,Plexus是一个数组数组;如果任何二级数组包含多个元素,那么该数组将根据强连接组件的定义描述一个具有循环的元素。然后,我必须(使用最小fas或近似值)删除边和/或顶点以使图非循环,并迭代运行tarjan's,直到每个scc子数组的长度为1。
我认为蛮力可能是解决最小fas的最佳方法:我不需要太聪明,因为我的数据集中任何scc中的节点数几乎都不会超过5或6。指数在5或6上是可以的。我严重怀疑我是否会有一个由数百个节点组成的SCC集,其中包含数十个不同的循环;这将是一个极其病态的最坏情况,我认为不会发生。不过,如果这样做了,运行时间将相当长。
基本上,我需要尝试删除图表弧的幂集,一次删除一个子集,子集按子集大小升序排序,然后"猜测并检查"图表是否仍然是循环的(Tarjan's),然后如果该幂集不固定循环,则将边添加回去。
如果边缘和节点的数量少于20个左右,这几乎是可以保证的,那么它不会占用大量的运行时间。
去掉迭代的tarjan肯定解决了我在快乐路径中的复杂性问题(没有循环或只是一个小循环),这是它给我最心痛的地方——而不是花25分钟来构建图表,它需要15秒。
经验教训:如果你的程序很慢,可能是因为你做了很多不必要的工作。在我的例子中,不必要的工作是对图中每添加一个新顶点执行Tarjan的拓扑排序,这只是因为我最初选择对数据建模的库的实现细节而需要的。