寻找图的割点或关节点算法的解释

34
我在网上搜索了DFS算法查找图的所有关节点的解释,但没有找到任何内容。甚至没有维基页面。
从阅读相关资料中,我了解到了基本信息。PDF 每个节点都有一个变量,实际上是查找反向边并找到最接近根节点的最高节点。在处理完所有边之后,它将被找到。
但是我不明白如何在执行DFS期间找到每个节点的这个向下和向上变量。这个变量究竟是做什么的?
请解释一下这个算法。
谢谢。

2
很抱歉,这个pdf中的算法是O(n^2)的。但它也提到了一种O(Edges)时间复杂度的算法。请解释一下那个算法。 - Ashish Negi
1
你可以尝试计算机科学论坛。 - akonsu
1
请问您能否发布此表格的链接?您是指我得到PDF文件的那所大学的链接吗? - Ashish Negi
1
这是一个与计算机科学相关的网站,名为“scicomp.stackexchange.com”。 - akonsu
1
@akonsu,讨论这个问题的更好地方是https://cs.stackexchange.com/。 - Shashank Shekhar
4个回答

46

寻找割点是深度优先搜索(DFS)的应用。

简要来说:

  1. 对图应用DFS。得到DFS树。
  2. 一个被早先访问的节点是它到达的和稍后访问的节点的“父节点”。
  3. 如果一个节点的任何子节点没有到达其父节点的任何祖先的路径,则意味着删除此节点将使该子节点与图不连通。
  4. 有一个例外:树的根节点。如果它有多于一个孩子,则它是一个割点,否则不是。

第3点实质上意味着该节点是一个割点。

现在对于一个孩子,这条到达节点祖先的路径将通过从它或其任何子节点之一出发的后向边来实现。

所有这些都在这个PDF中精美地解释了。


1
该PDF已被删除。您可以使用https://web.archive.org/web/20161130155755/https://www.eecs.wsu.edu/~holder/courses/CptS223/spr08/slides/graphapps.pdf。感谢Wayback Machine。 - vjmix

15

我将尝试以直观的方式解释该算法如何工作,并给出带注释的伪代码,输出双连通分量和桥。

实际上,可以为关节点开发蛮力算法。只需取出一个顶点,在图上运行BFS或DFS。如果保持连接,则该顶点不是关节点,否则它就是。这将在O(V(E + V))= O(EV)时间内运行。挑战在于如何在线性时间(即O(E + V))内完成此操作。

关节点连接两个(或多个)子图。这意味着没有从一个子图到另一个子图的边缘。因此,想象一下你在其中一个子图中并访问其节点。当您访问该节点时,您会标记它,然后使用某些可用边缘移动到下一个未被标记的节点。当您这样做时,如何知道您仍在同一个子图中?这里的见解是,如果您在同一个子图中,则在访问未标记的节点时,您最终会通过边缘看到已标记的节点。这称为反向边缘,并指示您有一个循环。一旦找到反向边缘,您可以确信所有通过那个已标记节点到您正在访问的节点的节点都是同一个子图的一部分,并且其中没有关节点。如果您没有看到任何反向边缘,则迄今为止访问的所有节点都是关节点。

因此,我们需要一种算法,该算法会访问顶点并标记目标边缘之间的所有点作为当前正在访问的节点在同一个子图中。很明显可能存在子图中的子图,因此我们需要选择到目前为止最大的子图。这些子图称为双连通分量。我们可以通过分配每个双连通分量的ID来实现此算法,该ID初始化为迄今为止访问的顶点数的计数。稍后,当我们找到反向边缘时,我们可以将双连通分量ID重置为迄今为止发现的最低ID。

我们显然需要两次传递。在第一遍中,我们要弄清楚从每个顶点通过反向边缘可以看到哪个顶点(如果有)。在第二遍中,我们想以相反的方向访问顶点并收集最小的双连通分量ID(即从任何后代可访问的最早祖先)。DFS自然适合于此处。在DFS中,我们先向下再回来,因此可以使用单个DFS遍历完成上述两次传递。

现在,不再拖延,以下是伪代码:

time = 0
visited[i] = false for all i
GetArticulationPoints(u)
    visited[u] = true
    u.st = time++
    u.low = v.st    //keeps track of highest ancestor reachable from any descendants
    dfsChild = 0    //needed because if no child then removing this node doesn't decompose graph
    for each ni in adj[i]
        if not visited[ni]
            GetArticulationPoints(ni)
            ++dfsChild
            parents[ni] = u
            u.low = Min(u.low, ni.low)  //while coming back up, get the lowest reachable ancestor from descendants
        else if ni <> parent[u] //while going down, note down the back edges
            u.low = Min(u.low, ni.st)

    //For dfs root node, we can't mark it as articulation point because 
    //disconnecting it may not decompose graph. So we have extra check just for root node.
    if (u.low = u.st and dfsChild > 0 and parent[u] != null) or (parent[u] = null and dfsChild > 1)
        Output u as articulation point
        Output edges of u with v.low >= u.low as bridges
    output u.low as bicomponent ID

这里似乎缺少与根节点相连的桥割点案例(直接与其同一组件中的根节点相连的桥割点将不被识别为关节点)。 - Marco A.
@MarcoA. - 我认为最后一个if语句中的第二个条件应该是这样做的(如果我正确理解了你的陈述)。顺便说一下,这个算法是由Hopcraft和Tarjan提出的,并且在他们的论文中已经得到了严格的证明。 - Shital Shah
如果您能解释一下为什么需要这个检查 ni <> parent[u],那会很有帮助。 - abhiarora

3

有一个似乎被所有说明遗漏的事实:

事实1:在深度优先搜索生成树(DFSST)中,每个返祖边将一个顶点连接到其祖先之一。

这对于算法的工作至关重要,这也是任意生成树不适用于该算法的原因。这也是根节点是割点的原因,当且仅当它有多于1个子节点时:不能在跨越树的根节点子树之间存在返祖边。

有关该语句的证明如下:假设(u,v)是一条返祖边,其中u不是v的祖先,(WLOG)u在DFS中先访问,然后p是u和v的最深公共祖先,那么DFS必须先访问p,然后访问u,然后再次访问p,然后才能访问v。但是在访问v之前无法重新访问p,因为u和v之间有一条边。


将V(c)称为DFSST中以c为根的子树的顶点集合
将N(c)称为具有V(c)(通过边缘或返祖边)内部邻居的顶点的集合

事实2:

对于非根节点u,
如果u有一个孩子c,使得N(c)⊆V(c)∪{u},那么u是割点。

原因:对于V(c)中的每个顶点w,从根到w的每条路径都必须包含u。否则,由于事实1,这样的路径必须包含连接u的祖先和u的后代的返祖边,从而使N(c)大于V(c)。

事实3:

事实#2的逆命题也是正确的。

原因:u的每个后代都具有不通过u的通向根的路径。 在V(c)中的后代可以通过连接V(c)与N(c)/ V(c)之间的返祖边来绕过u。


因此,对于算法,您只需要知道每个非根节点u的两件事:

  1. 该顶点的深度,称为D(u)
  2. N(u)的最小深度,也称为lowpoint,称为L(u)

因此,如果顶点u有一个孩子c,且L(c)小于D(u),则意味着以c为根的子树具有到达u的祖先的返祖边,从而根据事实3不是割点。反过来也是基于事实2。


谢谢您的解释。现在算法终于有了意义。 - Yasser Hussain

-1
如果u的后代的最小dfsnum大于u的dfsnum,则称u为关节点。
int adjMatrix[256][256];
int low[256], num=0, dfsnum[256];

void cutvertex(int u){
    low[u]=dfsnum[u]=num++;
    for (int v = 0; v < 256; ++v)
    {
        if(adjMatrix[u][v] && dfsnum[v]==-1)
        {
            cutvertex(v);
            if(low[v]>dfsnum[u])    
                cout<<"Cut Vertex: "<<u<<"\n";
            low[u]=min(low[u], low[v]);
        }
        else{
            low[u]=min(low[u], dfsnum[v]);
        }
    }
}

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