【问题标题】:How to detect if adding an edge to a directed graph results in a cycle?如何检测向有向图添加边是否会导致循环?
【发布时间】:2013-11-27 15:26:38
【问题描述】:

我遇到了wait-for graphs,我想知道,是否有任何有效的算法来检测向有向图添加边是否会导致循环?

有问题的图是可变的(它们可以添加或删除节点和边)。而且我们对真正了解违规循环并不感兴趣,只要知道有一个就足够了(以防止添加违规边缘)。

当然可以使用一种算法来计算强连接组件(例如 Tarjan 的)来检查新图是否是非循环的,但是每次添加边时再次运行它似乎效率很低。

【问题讨论】:

  • 你有记忆或时间限制吗?
  • @MarkElliot 这些图表用于协调许多并发运行的线程的锁定。我希望这个图有几十到几百个节点,边的数量可能是节点数量的二次方。图表会经常更新,所有更新都需要检查,如果不引起循环。
  • 一个幼稚的开始方法是在可以到达当前节点的节点的每个节点上携带一个数据结构。这将使循环检测在添加和更新时变得便宜,但增加了删除的复杂性。我想知道是否有更适合跟踪这个的数据结构(邻接矩阵?),但我仍在思考。
  • 从描述中听起来边缘移除将与添加一样频繁,这几乎排除了任何数据缓存,因为移除会使一切无效(或者至少我不知道如何避免这种情况)。因此,我的猜测是,最简单的解决方案(当添加边缘 A->B,从 B 填充并检查 A 是否无法访问)将是最有效的。
  • @PetrPudlák 你能改进你从托马斯的答案中遗漏的东西吗?我认为这个问题得到了回答。您希望在插入时检测周期?

标签: graph-theory directed-graph cyclic-graph


【解决方案1】:

如果我正确理解了您的问题,那么只有在之前没有从 v 到 u 的路径(即,如果 (u,v) 没有创建循环)时才插入新边 (u,v)。因此,您的图始终是 DAG(有向无环图)。在这种情况下,使用 Tarjan 的算法检测强连接组件 (http://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm) 听起来有点过头了。在插入 (u,v) 之前,你只需要检查是否存在从 v 到 u 的有向路径,这可以通过简单的 BFS/DFS 来完成。

所以最简单的方法是(n = |V|,m = |E|):

  • Inserting (u,v): 检查是否存在从 v 到 u 的路径(BFS/DFS)。时间复杂度:O(m)
  • 删除边:只需将它们从图中删除即可。时间复杂度:O(1)

虽然在最坏的情况下插入 (u,v) 需要 O(m) 时间,但在您的情况下它可能非常快。当从 v 开始执行 BFS/DFS 以检查 u 是否可到达时,您只访问从 v 可到达的顶点。我猜想在您的设置中,图形非常稀疏,另一个可到达的顶点数量不是那个高。

但是,如果您想提高理论运行时间,这里有一些提示(主要表明这不会很容易)。假设我们的目标是在 O(1) 时间内测试是否存在从 v 到 u 的有向路径。此上下文中的关键字是 DAG 的传递闭包(即,当且仅当 DAG 中存在从 u 到 v 的有向路径时,包含边 (u, v) 的图) .不幸的是,在动态环境中保持传递闭包似乎并不是那么简单。有几篇论文考虑了这个问题,我发现的所有论文都是 STOC 或 FOCS 论文,这表明它们非常参与。我发现的最新(也是最快)结果在 Sankowski (http://dl.acm.org/citation.cfm?id=1033207) 的论文 Dynamic Transitive Closure via Dynamic Matrix Inverse 中。

即使您愿意了解其中一种动态传递闭包算法(甚至想要实现它),它们也不会给您任何加速,原因如下。这些算法是为这样的情况设计的,您有大量的连接查询(然后可以在 O(1) 时间内执行)并且图表中只有很少的更改。然后的目标是使这些更改比重新计算传递闭包更便宜。但是,此更新仍然比单次检查连接要慢。因此,如果您需要对每个连接查询进行更新,最好使用上面提到的简单方法。

那么,如果它不符合您的需求,我为什么要提到这种维护传递闭包的方法呢?好吧,它表明搜索仅消耗 O(1) 查询时间的算法可能不会比使用 BFS/DFS 的简单算法更快地找到解决方案。您可以尝试的是获得比 O(m) 快但比 O(1) 更差的查询时间,而更新也比 O(m) 快。这是一个非常有趣的问题,但在我看来这是一个非常雄心勃勃的目标(所以也许不要花太多时间去尝试实现它..)。

【讨论】:

    【解决方案2】:

    正如 Mark 所建议的,可以使用存储连接节点的数据结构。最好使用布尔矩阵|V|x|V|。可以使用 Floyd–Warshall 算法初始化值。这是在O(|V|^3) 中完成的。

    T(i) 是一组具有到顶点i 的路径的顶点,而F(j) 是一组从顶点j 存在路径的顶点。第一个在i'th 行中为真,第二个在j'th 列中为真。

    添加边(i,j) 操作简单。如果ij 之前未连接,则对于来自T(i) 的每个a 和来自F(j) 的每个b 将矩阵元素(a,b) 设置为true。但操作并不便宜。最坏的情况是O(|V|^2)。那是在有向线的情况下,从端点添加边到开始顶点会使所有顶点连接到所有其他顶点。

    删除边(i,j) 不是那么简单,但在最坏的情况下不会更昂贵的操作:-) 如果删除边后有从ij 的路径,则没有任何变化。用 Dijkstra 检查,小于O(|V|^2)。不再连接的顶点是(a,b)

    • a in T(i) - i - T(j),
    • bF(j) + j

    只有T(j) 被删除边(i,j) 改变,所以它必须重新计算。这是通过从顶点j 沿相反的边缘方向进行的任何类型的图遍历(BFS、DFS)来完成的。这是在少于O(|V|^2) 的时间内完成的。由于矩阵元素的设置在最坏情况下再次为O(|V|^2),因此该操作与添加边具有相同的最坏情况复杂度。

    【讨论】:

      【解决方案3】:

      如果图形是定向的,您只需检查新边应该开始的节点的父节点(向上导航直到到达根节点)。如果其中一个父节点等于边的末端,则添加边将创建一个循环。

      【讨论】:

      • 图形不必是树。根可能可以通过不同的祖先访问,您必须检查所有这些。并不是说这特别昂贵,但它一个潜在的 O(|E|) 解决方案——这正是@Thomas 所提议的。
      【解决方案4】:

      如果之前的所有作业都按拓扑排序。然后,如果您添加一条看起来会阻碍排序并且无法修复的边,那么您就有了一个循环。

      https://stackoverflow.com/a/261621/831850

      所以如果我们有一个排序的节点列表:

      1, 2, 3, ..., x, ..., z, ...
      Such that each node is waiting for nodes to its left.
      

      假设我们要从 x->z 添加一条边。好吧,这似乎阻碍了这种排序。因此,我们可以将 x 处的节点移动到位置 z+1,如果没有节点 (x, z] 与 x 处的节点有边,则将修复排序。

      【讨论】:

        猜你喜欢
        • 2013-01-03
        • 1970-01-01
        • 2016-03-28
        • 1970-01-01
        • 2018-11-30
        • 2017-01-19
        • 2011-02-08
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多