【问题标题】:Why are dependency graphs not represented as bi-directional acyclic graphs?为什么依赖图不表示为双向无环图?
【发布时间】:2015-03-12 09:01:36
【问题描述】:

我知道依赖图(比如在安装过程中找出哪个包依赖于哪个包)可以表示为有向无环图。

a
|--> b
|    |--> d
|    `--> e
|         |
|         |
`--> c <--'

例如,上图代表如下。

  • a 取决于 b、c、d、e
  • b 取决于 d、e、c
  • c 不依赖任何东西
  • d 不依赖任何东西
  • e 取决于 c

这个图可以帮助我们回答某个包在线性时间内依赖什么,即 O(n),其中 n 是图中包和边的总数。示例: a 依赖于哪些包?结果是:b、c、d、e。

它可以帮助我们回答简单的问题,例如某个包在恒定时间内立即依赖什么。示例: a 立即依赖哪些包?结果是:b,c。

但它不能回答一个简单的问题,比如在恒定时间内立即依赖于某个包。示例:哪些包直接依赖于 c?结果是:a 和 e。要回答这个简单的问题似乎需要对图进行完整搜索,因此需要线性时间。如果每个子顶点保持到其父顶点的反向链接,同时仍保持子顶点和父顶点之间的区别,则可以改进这一点。

如果我们引入从每个子顶点到其父顶点的反向链接,它就变成了一个双向无环图,并且它似乎简化了许多图搜索算法。

我有以下问题。

  1. 此类依赖关系图有正式名称吗?
  2. 为什么我们在计算理论的研究中看不到双向无环图?
  3. 在依赖图的实际实现中是否使用了这种双向图?例子?

【问题讨论】:

    标签: algorithm data-structures graph dependencies


    【解决方案1】:

    如果您添加没有语义意义的反向链接,并且只会加快“谁指代我”的搜索速度,那仍然是一个 DAG。同样,搜索树中的父链接不会将树变成搜索“图”。这是一个没有语义或数学意义的实现细节。因此,它没有单独研究(最多在讨论复杂性时顺便提及)。

    此外,人们可以灵活地选择边缘(依赖 -> 用户或用户 -> 依赖),两者都根据需要使用。我想不出在同一个图中需要两者的许多用例。即便如此,在需要时仅反转整个图的边缘(单个 O(n) 操作)可能更有利可图。

    由于这些原因,这种优化通常不会被赋予单独的名称。如果需要澄清的话,它只是“一个 DAG”,一个“(带后边)”。

    【讨论】:

      【解决方案2】:

      您在这里谈论的第一件事(当 a->b 和 b->c 时生成所有边,如 a->c)称为transitive closure。它本身是有用的、有趣的和研究的。但是,显式存储所有此类边将导致图所需的存储空间(可能是二次的)爆炸,因为在具有 |V| 个节点的完整图中,您有 O(| V|2) 个边。所以这是空间和时间复杂度之间的权衡:如果你存储所有(前向)边,你可以更快地(前向)遍历图,在你观察到的恒定时间内,但你付出了存储的代价。

      虽然您没有问过这个问题,但我要指出,显式存储传递闭包对于依赖图可能是不可取的。以包管理器为例:您希望它快速找出直接依赖项,以检查它们是否已安装以及是否可能将缺少的依赖项添加到安装多个包的事务中。但是,在这种情况下,启用对包的所有(直接和间接)依赖项的恒定时间访问似乎并不是特别有用,因为大多数间接依赖项无论如何都可能得到满足。您只会得到一个更大的列表来查看,并且可能会得出结论,大多数都已安装。


      您正在谈论的另一件事,即每条边都反转的图形,称为transpose graph。请注意,如果将“直接”图和转置图都存储在同一数据结构中,则需要对边缘进行 [bi] 着色(使用不同命名的成员指针/引用)。以这种方式将它们存储在一起是相当微不足道的,所以我想这就是为什么你没有看到太多提及它的原因。一些图算法工作/书籍do assume 这样的有向图表示,即传入和传出边都存储在每个顶点的单独(双链接)列表中。尽管许多(介绍性)教科书确实没有谈论它(大概是为了保持演示简单),但这种表示(即传入和传出列表)is used in practice, for example in LEDA。这是来自a LEDA presentation 的一张幻灯片,详细介绍了它们的静态(即假设固定)图形数据结构;动态的会有双向链表而不是数组。我将单向(“定向”)和它们的“双向”表示都包括在内,以便于比较:

      Boost 有一个类似的功能,虽然它只是针对他们的adjacency list implementation 的一个调整(称为bidirectionalS):

      bidirectionalS 选择器指定图形将提供 in_edges() 函数以及 out_edges() 函数。这会给每条边带来两倍的空间开销,这就是为什么 in_edges() 是可选的。

      请记住,因为您可以区分两组边缘(在 LEDA 的情况下通过“先进”和“先出”或在提升的情况下通过 in_edges() 和 out_edges)您在这里真正拥有什么在数学上是一个带有转置的有向图的disjoint union。如果你失去了两组边(指针)之间的区别/颜色,你得到的有时被称为双向图,尽管这个术语(就像图论中的许多人一样)is unfortunately overloaded。如果您希望 LEDA 的术语 双向图 以某种方式对其含义进行标准化,它实际上是 more likely to mean the same thing as bidirected graph to theorists

      总结一下我目前的回答:

      1. 我不认为像这样存储的依赖关系图有一个名称,但对于一般双向表示的图来说,这将是一个合理的名称,不幸的是,它在一些软件包之外并没有这样出现。双向(或双向)图是一个更广泛的术语,但大多数理论家可能会认为你的意思是你不能再区分两组边之间的区别(即他们会假设你的意思是联合而不是与转置的不相交联合图表。)

      2. 似乎它主要是在实际实施环境中讨论的一个方面(如 LEDA 或 boost),因此理论和介绍书籍似乎不太关心它。

      至于包存储库的实际表示 (3),您似乎忽略了大多数(据我所知)将存储 AND、OR 和 NOT 约束以额外处理替代方案和冲突。您只能使用我们上面讨论的依赖图来处理 AND。一旦你添加了这些额外的 OR & NOT 特性,你就可以更难地解决(NP-complete)SAT problems 只是为了安装一些东西;参见the Opium paper (2007) 进行讨论;对于最近的一个(2010 年),请参阅Apt-pbo paper。因此,相比之下,恒定时间反向依赖查找开始显得微不足道。但要真正回答您的问题:

      1. 我查看了 apt 源,它确实将反向依赖项单独存储在其缓存中(您 query with apt-cache)。对于每个包(在pkgcache.h 中定义的pkgCache::Package),都有一个RevDepends linked list,并且每次安装或删除某些东西时都会更新它:在depcache.cc:pkgDepCache::Update 中,它具有执行(除其他外)Update(P.ParentPkg().RevDependsList()); 的 for 循环。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-11-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-12-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多