【问题标题】:Modified BFS Time Complexity修改后的 BFS 时间复杂度
【发布时间】:2017-11-07 17:26:30
【问题描述】:

所以当队列这样实现时,BFS 的复杂度为 O(|V| + |E|):

ENQUEUE(Q,source_vertex)
while Q != NULL
   u=DEQUEUE(Q)
   for each v in AdjacencyList[u]
     if v not yet visited
        Make v visited
        ENQUEUE(Q,v)

如果我修改代码以将 u 的邻接列表中的所有顶点添加到队列中,如下所示:

ENQUEUE(Q,source_vertex)
while Q != NULL
   u=DEQUEUE(Q)
   for each v in AdjacencyList[u]
      if v not finalized            
         ENQUEUE(Q,v)
   make u finalized

运行时间还会保持O(|V| + |E|)吗?

提前致谢。

【问题讨论】:

  • 如果图有一个循环,你可能会一遍又一遍地将同一个节点排入队列,导致无限循环。还是您的意思不同?
  • 嘿,是的,我意识到了,所以我现在编辑了代码。
  • 你得到了相互矛盾的答案。我建议您尝试使用不断增加的完整图表来向自己证明答案是

标签: algorithm graph-theory graph-algorithm breadth-first-search


【解决方案1】:

假设您有一个包含 n 个节点的集团(让我们将它们编号为 1、2、...、n,并假设邻接表按此顺序存储它们)并且您从节点 1 开始运行修改后的算法。节点 1 将入队节点 2, 3, ..., n 总共 Θ(n) 工作。队列现在看起来像这样:

2, 3, 4, ..., n

当您随后处理节点 2 时,它将查看其所有边以进行 Θ(n) 更多工作,然后将节点 3、4、5、...、n 排入队列。我们的队列现在看起来像这样:

3, 4, 5, ..., n, 3, 4, 5, ..., n

我们现在处理节点 3,它查看它的所有边以进行 Θ(n) 工作,然后将节点的 4、5、6、...、n 排入队列,因此我们的队列如下所示:

4, 5, 6, ..., n, 3, 4, 5, ..., n, 4, 5, 6, ..., n

这里的模式是我们最终将图中每个节点的许多副本排入队列。事实上,随着时间的推移,我们最终会在队列中得到总共 Θ(n2) 个节点,并且我们对每个节点做 Θ(n) 个工作。这意味着该图完成的总工作量为 Θ(n3),超过了原始 BFS 实现的 O(m + n) 时间限制。

因此,这种新实现可能会比常规 BFS 渐进地慢。

【讨论】:

  • 如果输入图是有向无环图怎么办?
  • 想象一个完整的有向无环图,再次将节点编号为 1、2、3、...、n,以便所有内容都指向它之后的所有节点。节点 2 插入一次,节点 3 插入两次,节点 4 插入 3 次,依此类推。此外,节点号 k 有 (n - 1 - k) 条边离开它。完成的总功为 1(n - 2) + 2(n - 3) + 3(n - 4) + 4(n - 5) + ... + (n - 3) 2 + (n - 2) 1 + (n - 1)0。如果你算出这个总和,它会再次得到 Theta(n^3),所以这仍然比常规 BFS 慢。
  • 更改示例以将边添加到队列并处理边的非最终端 - 您会发现每条边都添加到队列中一次(以及边的非最终端与您示例中的顶点相同)但是由于您在n 顶点上有一个完整的图形,因此您将拥有n(n-1)/2 边,因此算法仍然是O(|V| + |E|) 它只是|E| 与@987654325 的顺序相同@.
  • @MT0 用于已处理的无向案例结束,不会放回队列中,但仍会被内部循环重新扫描。正是这项工作花费了多次重新处理边缘,这些工作加起来并导致问题。除非我弄错了?
  • @MT0 虽然就入队总数而言,我完全支持你。这部分论点是完全正确的。
【解决方案2】:

让我们更改您的算法以将边添加到队列中(因为这就是您正在做的隐含操作 - 除非您在将其添加到队列时只查看相反的顶点而不是整个边):

ENQUEUE( Q,(NULL->source_vertex) )            # start with a dummy edge
WHILE Q != NULL
  (s->u)=DEQUEUE(Q)
  for each (u->v) in AdjacencyList[u]   
    if v not finalized
      ENQUEUE(Q,(u->v))
  make u finalized

每个边都将被视为两次入队 (u->v)(v->u)。当访问边u 的第一个顶点时,每个相邻边将被放入队列中,然后u 被最终确定。当访问v 时,将考虑边缘(v->u),但由于u 已经完成,因此不会将其添加到队列中;所以每条边只会被排队一次(在一个方向上而不是在另一个方向上)。

算法的问题在于它不检查它要处理的顶点是否已经完成,并且会为队列中的每条边重新处理它,再次遍历所有相邻边,使你的算法O(|V||E|)(而不是比O(|V| + |E|))。

一个简单的解决方法是:

ENQUEUE( Q,(NULL->source_vertex) )            # start with a dummy edge
WHILE Q != NULL
  (s->u)=DEQUEUE(Q)
  if u not finalized
    for each (u->v) in AdjacencyList[u]
      if v not finalized
        ENQUEUE(Q,(u->v))
    make u finalized

此外,您的算法的两个版本都将从一个顶点开始,然后在同一个连通组件中处理每条边 - 但是它们只会在连通图上执行完整的 BFS。如果您有多个断开连接的组件,那么您需要使用:

for each source_vertex in Graph
  if source_vertex not visited/finalised
    call your_algorithm( source_vertex )

然后它将是O(|V| + |E|) 并且将访问所有顶点,而不管图是否连接。

【讨论】:

    【解决方案3】:

    据我了解,运行时复杂度也是O(|V|+|E|)。第二个版本只是将finalization步骤(也可以称为visiting)推迟到下一次迭代;递归调用的次数和顶点的顺序都不会改变。

    【讨论】:

    • 循环不会是无限循环吗?
    • 我不这么认为; u 在循环结束时被最终确定,并且最终节点永远不会放入队列中,因此队列最终必须为空。
    • 我想我对这种说法有一个反例。你能检查我的答案,看看我是否遗漏了什么吗?
    猜你喜欢
    • 2020-12-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-22
    • 1970-01-01
    • 2017-10-29
    • 1970-01-01
    相关资源
    最近更新 更多