给定一个无环有向图,返回一个节点“在同一层级”上的集合的集合?

4

首先,我不确定这种算法称为什么,这是主要问题——因此第一个问题是这个算法叫什么?

基本上,我有一个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]]

编辑:如果有帮助,请让我添加一些限制。 - 这里没有循环,这是保证的 - 没有一个起点可以成为图形的开始

我要做的是收集在同一层上的节点,以便它们的处理可以并行化,但在外部集合中,处理是串行的。

编辑2:显然我没有想过这个问题,所以最容易描述“级别”的方法是以最深祖先为单位,所有具有相同深度的前级的节点都被包含在内。因此,上面列表中的第一个条目是所有具有0作为最深祖先的节点,第二个条目具有1个祖先,第三个条目具有2个祖先,依此类推。在每个列表中,兄弟姐妹的顺序是无关紧要的,因为它们将被并行处理。


BFS, 广度优先搜索?https://zh.wikipedia.org/wiki/广度优先搜索 - Dmitry Bychenko
你所询问的似乎对图形结构做出了许多假设(例如,从任何节点到任何其他节点最多只有一条路径)。如果将边[1,2]添加到您的图形中,您希望集合是什么? - BrenBarn
我已经修改了标题,以尽可能准确地反映您问题的意图。希望我没有误解... - holdenweb
你能清晰地定义什么是“层级”吗?如果有一条边(1,2)和其他的边(3,4)和(4,2),你想要哪些集合? - Joel
@ Joel,这样清楚一些吗? - Nim
3个回答

5
你的问题说明,你想要这个图的输出结果为[[1, 2, 4, 6], [3], [5], [7], [8, 9, 10]]。如果我理解正确,模式如下:
  • [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
谢谢您的建议,但是我决定采取略微不同的方法,即在每个节点处保留一个指示其级别的属性。当我添加新节点并重新排列树时,我遍历后继节点并跟踪最大深度,有点类似于下面Charles的回答。通常,这些操作应该是相当不频繁的。最常见的操作是评估树,您的第二种方法太昂贵了,一个拓扑排序,然后通过一次遍历树查看属性将允许我做到完全符合我的需要。 - Nim
抱歉 - 我以为它可以进一步简化 - 但我错了。不过,正如我上面所说的,通过存储额外的状态,我可以避免破坏图形。 - Nim
@Nim 当然,你用什么方法都可以。我只想指出我所知道的最有效的实现方式是使用Fibonnacci堆的末尾答案。那个方法不需要破坏图形,而只需要向每个边添加一个属性(指示它是否已被丢弃为起源于已使用的顶点)。 - Ami Tavory

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;
            }
        }
    }
}

我认为我曾经将此应用于同样的问题,然后意识到这是不必要的。只需设置任务的触发器,所有任务都通过条件变量和promise向其依赖项发出完成信号。因此,我只需要知道每个任务的依赖关系,找到初始任务,然后启动即可。在您的情况下,是否需要完整的run_level规范?


有趣!这个问题被标记为“networkx”,但我很期待查看您的代码,看看如何在BGL中实现。 - Ami Tavory
我试图避免在树中使用过多的状态,但是似乎持有一个run_level并结合拓扑排序能够帮助我在单次遍历中处理树,这样我就不需要像你描述的那样使用倒计时门闩了,我知道一旦到达下一个运行级别,所有之前的节点都已经被评估了。 - Nim

0
为什么 BFS 不能解决它?BFS 算法是一种广度优先遍历算法,即它按层遍历树。这也意味着,同一层的所有节点会被同时遍历,这正是您想要的输出结果。 正如评论中指出的那样,这将假定图中有一个起始点。

1
BFS 的问题在于它假设了一些起始节点,而这个问题似乎想要获取关于整个图的这些节点集合作为信息。 - BrenBarn
@brenbarn: 首先创建图G',它由G的所有节点和边以及一个新节点n'组成,对于G中入度为0的每个节点q,都有边n'->q。 (您可以通过添加n'->q来完成O(|E+N|),然后在所有边上进行一次遍历并将边的头从已添加的边的集合中删除。)如果没有添加边,则图是循环的,并且节点“级别”的概念有点奇怪。 但是,您可以使用Tarjan算法识别SCC,然后从SCC诱导图中创建增广图。 - rici
1
你可以使用BFS算法来实现,但它并不像你的答案所暗示的那样简单。具体来说,"所有在同一层级的节点同时被遍历"这个说法是错误的。例如,有多种方法可以到达节点7。可能的路径有{1,3,5,7}、{2,3,5,7}、{4,5,7}和{6,7}。因此,BFS将在第2、3和4层找到节点7。节点7的正确层级是4。因此,BFS需要跟踪任何节点被发现的最深层级。(当然,在BFS开始之前,您还需要找到起始节点,如前所述。) - user3386109

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接