【问题标题】:Given an acyclic directed graph, return a collection of collections of nodes "at the same level"?给定一个无环有向图,返回“同一级别”的节点集合的集合?
【发布时间】:2016-09-16 18:52:21
【问题描述】:

首先我不确定这种算法叫什么,这是主要问题 - 所以问题的第一部分是这个算法叫什么?

基本上我有一个DiGraph() 插入节点[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 和边缘([1,3],[2,3],[3,5],[4,5],[5,7],[6,7],[7,8],[7,9],[7,10])

由此我想知道是否有可能获得如下集合:[[1, 2, 4, 6], [3], [5], [7], [8, 9, 10]]

编辑:如果有帮助,让我添加一些约束。 - 没有周期,这是有保证的 - 图表没有一个起点

我正在尝试做的是收集同一级别的节点,以便它们的处理可以并行化,但在外部集合中,处理是串行的。

EDIT2:很明显,我没有充分考虑这一点,所以描述“级别”的最简单方法是最深的前辈,所有节点都具有相同的前辈深度。所以上面列表中的第一个条目是所有以 0 作为最深前驱的节点,第二个有一个,第三个有两个,依此类推。在每个列表中,兄弟姐妹的顺序无关紧要,因为它们将被并行处理。

【问题讨论】:

  • BFS,广度优先搜索? en.wikipedia.org/wiki/Breadth-first_search
  • 您所要求的似乎对图结构做出了许多假设(例如,从任何节点到任何其他节点最多只有一条路径)。如果将边 [1, 2] 添加到图表中,您希望集合是什么?
  • 我已修改标题以更准确地反映您的问题的意图。希望我没有误会...
  • 你能清楚地定义什么是“级别”吗?如果有一条边 (1,2) 和一些其他边 (3,4) 和 (4,2) 怎么办。你想要什么收藏品?是
  • @joel,这样更清楚吗?

标签: python algorithm graph graph-theory networkx


【解决方案1】:

您的问题表明您希望此图表的输出为[[1, 2, 4, 6], [3], [5], [7], [8, 9, 10]]。 IIUC,模式如下:

  • [1, 2, 4, 6] 是没有入边的节点。

  • [3] 是没有入边的节点,假设之前的所有节点都被删除了。

  • [4] 是没有入边的节点,假设之前的所有节点都被删除了。

  • 等等。 (直到所有节点都被擦除)

假设我们开始

g = networkx.DiGraph()
g.add_edges_from([[1,3],[2,3],[3,5],[4,5],[5,7],[6,7],[7,8],[7,9],[7,10]])

那么我们就可以这样编码了

def find_levels(g):
    levels = []
    while g.nodes():
        no_in_nodes = [n for (n, d) in g.in_degree(g.nodes()).items() if d == 0]
        levels.append(no_in_nodes)
        for n in no_in_nodes:
            g.remove_node(n)
    return levels

如果我们运行这个,我们会得到结果:

>>> find_levels(g)
[[1, 2, 4, 6], [3], [5], [7], [8, 9, 10]]

这里的复杂度是Θ(|V|2 + |E|)。可以使用Fibonnacci Heap 构建稍微复杂一些的版本。基本上,所有顶点都需要放入一个堆中,每一层由度数为 0 的顶点组成。每次弹出一个,并且删除其他顶点的边时,我们可以将其转换为堆减少键操作(减少剩余顶点的入度)。这会将运行时间减少到 Θ(|V| log(|V|) + |E|)

【讨论】:

  • 这里的问题是节点不能被删除,例如当你查看1时,3/5/7的“级别”发生了变化..
  • 感谢您的输入,但是,我决定采用一种稍微不同的方法,即在每个节点上保留一个属性来指示它的级别。当我添加新节点并重新排列树时,我会遍历后继节点并跟踪最大深度。有点像下面查尔斯的回答。通常,这些操作应该很少见。最常见的操作是评估树,而您的第二种方法太昂贵了,拓扑排序,然后通过树单次查看属性将允许我做我需要的事情..
  • 对不起 - 我认为它可以进一步简化 - 但我错了。正如我上面所说,通过存储额外的状态,我可以绕过而不必破坏图形..
  • @Nim 当然,任何适合你的。我只想指出,我所知道的最有效的实现是在我的答案末尾使用斐波那契堆。那个不需要破坏图,而只是为每条边添加一个属性(表明它是否已经被丢弃,因为它是从一个使用过的顶点发出的)。
【解决方案2】:

正如 Ami 所说,拓扑排序将实现这一点。下面是一个 Boost Graph Library 的实现,没有上下文,但是可以提取伪代码。 toporder 对象只是为拓扑排序提供迭代器。如果需要,我可以提取通用算法。

template<typename F>
void 
scheduler<F>::set_run_levels()
{

  run_levels = std::vector<int>(tasks.size(), 0);
  Vertexcont toporder;

  try
    {
      topological_sort(g, std::front_inserter(toporder));
    }
  catch(std::exception &e)
    {
      std::cerr << e.what() << "\n";
      std::cerr << "You most likely have a cycle...\n";
      exit(1);
    }

  vContIt i = toporder.begin();

  for(;
      i != toporder.end();
      ++i)
    {
      if (in_degree(*i,g) > 0)
        {
          inIt j, j_end;
          int maxdist = 0;
          for(boost::tie(j,j_end) = in_edges(*i,g);
              j != j_end;
              ++j)
            {
              maxdist = (std::max)(run_levels[source(*j,g)], maxdist);
              run_levels[*i] = maxdist+1;
            }
        }
    }
}

我想我曾经将它应用于同样的问题,然后意识到它是不必要的。只需在触发条件下设置任务,所有任务都会向其依赖项发出完成信号(通过条件变量,承诺)。所以我只需要知道每个任务的依赖关系,找到初始任务,然后触发。您的情况是否需要完整的 run_level 规范?

【讨论】:

  • 有趣!该问题被标记为networkx,但我期待着查看您的代码并了解如何在 BGL 中执行此操作。
  • 我试图避免树中的状态过多,但似乎持有 run_level 结合拓扑排序有助于我一次性处理树,那么我不需要正如你所描述的倒计时锁存器,我知道当我进入下一个运行级别时,之前的所有节点都已被评估..
【解决方案3】:

为什么 bfs 不解决呢? bfs 算法是广度遍历算法,即它遍历树级别。这也意味着,一次遍历同一级别的所有节点,这是您想要的输出。 正如评论中指出的那样,这将假定图表中的一个起点。

【讨论】:

  • 关于 BFS 的事情是它假设了一些起始节点,而问题似乎希望将这些节点集作为关于整个图的信息。
  • @brenbarn:首先创建图 G',它由 G 的所有节点和边加上一个新节点 n' 和边 n'->q 对于 G 中的每个节点 q 和 in-度数 0。(您可以在 O(|E+N|) 中做到这一点,方法是为所有 q 添加 n'->q,然后通过所有边并从添加的边集中删除边的头部。)如果没有边相加,则图是循环的,节点“级别”的概念有点奇怪。但是,您可以使用 Tarjan 算法来识别 SCC,然后从 SCC 的诱导图创建增强图。
  • 您可以使用 BFS 来做到这一点,但这并不像您的回答所暗示的那么简单。具体来说,“一次遍历同一级别的所有节点”的说法是错误的。例如,有多种方法可以到达节点 7。可能的路径是 {1,3,5,7}、{2,3,5,7}、{4,5,7} 和 {6,7 }。所以 BFS 会在 2、3 和 4 层找到节点 7。节点 7 的正确层是 4。所以 BFS 需要跟踪找到任何节点的最深层。 (当然,如前所述,您还需要在 BFS 开始之前找到起始节点。)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-07-10
  • 2014-12-26
  • 2014-09-17
相关资源
最近更新 更多