tarjin算法 割点割边

Tarjan算法:求解图的割点与桥(割边)

void tarjan(int u, int root)
{
    int child = 0;
    dfn[u] = low[u] = ++id;
    for (size_t i = 0; i < G[u].size(); ++i) {
        int v = G[u][i];
        if (dfn[v] == 0) {
            tarjan(v, root);
            low[u] = min(low[u], low[v]);//为能访问的最原始祖先
            if (u != root && low[v] >= dfn[u]) cut[u] = true;//如果某点必须通过这个点访问它或者它的祖先  这个点是割点
            if(low[v]>dfn[u]) bridge[u]=1;//割边
            if (u == root) child++;//点的孩子加
        }
        low[u] = min(low[u], dfn[v]);//这个点所能到达的最大祖先
    }
    if (u == root && child >= 2) cut[root] = true;//如果是根节点,并且不是一个孩子,那么是割点
}





简介

割边和割点的定义仅限于无向图中。我们可以通过定义以蛮力方式求解出无向图的所有割点和割边,但这样的求解方式效率低。Tarjan提出了一种快速求解的方式,通过一次DFS就求解出图中所有的割点和割边。

欢迎探讨,如有错误敬请指正

如需转载,请注明出处 http://www.cnblogs.com/nullzx/


1. 割点与桥(割边)的定义

在无向图中才有割边和割点的定义

割点:无向连通图中,去掉一个顶点及和它相邻的所有边,图中的连通分量数增加,则该顶点称为割点。

桥(割边):无向联通图中,去掉一条边,图中的连通分量数增加,则这条边,称为桥或者割边。

割点与桥(割边)的关系

1)有割点不一定有桥,有桥一定存在割点

2)桥一定是割点依附的边。

下图中顶点C为割点,但和C相连的边都不是桥。

image

2. 暴力解决办法解决求解割点集和割边集

暴力法的原理就是通过定义求解割点和割边。在图中去掉某个顶点,然后进行DFS遍历,如果连通分量增加,那么该顶点就是割点。如果在图中去掉某条边,然后进行DFS遍历,如果连通分量增加,那么该边就是割边。对每个顶点或者每个边进行一次上述操作,就可以求出这个图的所有割点和割边,我们称之为这个图的割点集和割边集。

在具体的代码实现中,并不需要真正删除该顶点和删除依附于该顶点所有边。对于割点,我们只需要在DFS前,将该顶点对应是否已访问的标记置为ture,然后从其它顶点为根进行DFS即可。对于割边,我们只需要禁止从这条边进行DFS后,如果联通分量增加了,那么这条边就是割边。

3. Tarjan算法的原理

判断一个顶点是不是割点除了从定义,还可以从DFS(深度优先遍历)的角度出发。我们先通过DFS定义两个概念。

假设DFS中我们从顶点U访问到了顶点V(此时顶点V还未被访问过),那么我们称顶点U为顶点V的父顶点,V为U的孩子顶点。在顶点U之前被访问过的顶点,我们就称之为U的祖先顶点

显然如果顶点U的所有孩子顶点可以不通过父顶点U而访问到U的祖先顶点,那么说明此时去掉顶点U不影响图的连通性,U就不是割点。相反,如果顶点U至少存在一个孩子顶点,必须通过父顶点U才能访问到U的祖先顶点,那么去掉顶点U后,顶点U的祖先顶点和孩子顶点就不连通了,说明U是一个割点。

1

上图中的箭头表示DFS访问的顺序(而不表示有向图),对于顶点D而言,D的孩子顶点可以通过连通区域1红色的边回到D的祖先顶点C(此时C已被访问过),所以此时D不是割点。

2

上图中的连通区域2中的顶点,必须通过D才能访问到D的祖先顶点,所以说此时D为割点。再次强调一遍,箭头仅仅表示DFS的访问顺序,而不是表示该图是有向图。

这里我们还需要考虑一个特殊情况,就是DFS的根顶点(一般情况下是编号为0的顶点),因为根顶点没有祖先顶点。其实根顶点是不是割点也很好判断,如果从根顶点出发,一次DFS就能访问到所有的顶点,那么根顶点就不是割点。反之,如果回溯到根顶点后,还有未访问过的顶点,需要在邻接顶点上再次进行DFS,根顶点就是割点。

4. Tarjan算法的实现细节

在具体实现Tarjan算法上,我们需要在DFS(深度优先遍历)中,额外定义三个数组dfn[],low[],parent[]

4.1 dfn数组

dnf数组的下标表示顶点的编号,数组中的值表示该顶点在DFS中的遍历顺序(或者说时间戳),每访问到一个未访问过的顶点,访问顺序的值(时间戳)就增加1。子顶点的dfn值一定比父顶点的dfn值大(但不一定恰好大1,比如父顶点有两个及两个以上分支的情况)。在访问一个顶点后,它的dfn的值就确定下来了,不会再改变。

4.2 low数组

low数组的下标表示顶点的编号,数组中的值表示DFS中该顶点不通过父顶点能访问到的祖先顶点中最小的顺序值(或者说时间戳)。

每个顶点初始的low值和dfn值应该一样,在DFS中,我们根据情况不断更新low的值。

假设由顶点U访问到顶点V。当从顶点V回溯到顶点U时,

如果

dfn[v] < low[u]

那么

low[u] = dfn[v]

如果顶点U还有它分支,每个分支回溯时都进行上述操作,那么顶点low[u]就表示了不通过顶点U的父节点所能访问到的最早祖先节点。

4.3 parent数组

parent[]:下标表示顶点的编号,数组中的值表示该顶点的父顶点编号,它主要用于更新low值的时候排除父顶点,当然也可以其它的办法实现相同的功能。

4.4 一个具体的例子

现在我们来看一个例子,模仿程序计算各个顶点的dfn值和low值。下图中蓝色实线箭头表示已访问过的路径,无箭头虚线表示未访问路径。已访问过的顶点用黄色标记,未访问的顶点用白色标记,DFS当前正在处理的顶点用绿色表示。带箭头的蓝色虚线表示DFS回溯时的返回路径。

1)

3

当DFS走到顶点H时,有三个分支,我们假设我们先走H-I,然后走H-F,最后走H-J。从H访问I时,顶点I未被访问过,所以I的dfn和low都为9。根据DFS的遍历顺序,我们应该从顶点I继续访问。

2)

4

上图表示由顶点I访问顶点D,而此时发现D已被访问,当从D回溯到I时,由于

dfn[D] < dfn[I]

说明D是I的祖先顶点,所以到现在为止,顶点I不经过父顶点H能访问到的小时间戳为4。

3)

image

根据DFS的原理,我们从顶点I回到顶点H,显然到目前为止顶点H能访问到的最小时间戳也是4(因为我们到现在为止只知道能从H可以通过I访问到D),所以low[H] = 4

4)

image

现在我们继续执行DFS,走H-F路径,发现顶点F已被访问且dfn[F] < dfn[H],说明F是H的祖先顶点,但此时顶点H能访问的最早时间戳是4,而F的时间戳是6,依据low值定义low[H]仍然为4。

5)

image

最后我们走H-J路径,顶点J未被访问过所以 dfn[J] = 10   low[J] = 10

6)

image

同理,由DFS访问顶点B,dfn[J] > dfn[B],B为祖先顶点,顶点J不经过父顶点H能访问到的最早时间戳就是dfn[B],即low[J] = 2

7)

image

我们从顶点J回溯到顶点H,显然到目前为止顶点H能访问到的最早时间戳就更新为2(因为我们到现在为止知道了能从H访问到J),所以low[H] = 2

8)

image

根据DFS原理,我们从H回退到顶点E(H回退到G,G回退到F,F回退到E的过程省略),所经过的顶点都会更新low值,因为这些顶点不用通过自己的父顶点就可以和顶点B相连。当回溯到顶点E时,还有未访问过的顶点,那么继续进行E-K分支的DFS。

9)

image

从E-K分支访问到顶点L时,顶点k和L的的dfn值和low值如图上图所示

10)

image

接着我们继续回溯到了顶点D(中间过程有所省略),并更新low[D]

11)

image

最后,按照DFS的原理,我们回退到顶点A,并且求出来了每个顶点的dfn值和low值。

4.5 割点及桥的判定方法

割点:判断顶点U是否为割点,用U顶点的dnf值和它的所有的孩子顶点的low值进行比较,如果存在至少一个孩子顶点V满足low[v] >= dnf[u],就说明顶点V访问顶点U的祖先顶点,必须通过顶点U,而不存在顶点V到顶点U祖先顶点的其它路径,所以顶点U就是一个割点。对于没有孩子顶点的顶点,显然不会是割点。

桥(割边):low[v] > dnf[u] 就说明V-U是桥

需要说明的是,Tarjan算法从图的任意顶点进行DFS都可以得出割点集和割边集。

image

从上图的结果中我们可以看出,顶点B,顶点E和顶点K为割点,A-B以及E-K和K-L为割边。

代码:

#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
int n,m;
int head[100005];
int bel[100005];
int dfn[200005];
int low[200005];
int stack[100005];
int to[200005];
int nex[200005];
int fa[200005];
int tot;
int cnt=1;
bool cut[200005];
inline void add(int x,int y){
    ++tot;
    nex[tot]=head[x];
    head[x]=tot;
    to[tot]=y;
}
void tarjan(int p){
    int rd=0;
    dfn[p]=low[p]=++cnt;
    for(int i=head[p];i;i=nex[i]){
        int v=to[i];
        if(!dfn[v]){
            fa[v]=fa[p];
            tarjan(v);
            low[p]=min(low[p],low[v]);
            if(low[v]>=dfn[p]&&p!=fa[p])cut[p]=true;
             //非根且子树能达到的dfn最小的结点的时间>=自己的时间时
            //说明他的子树中最早能访问到的结点都比他后访问,只要不为根就一定是割点(注意根例外)
            if(p==fa[p]) rd++;
        }
        low[p]=min(low[p],dfn[v]);
    }
    if(p==fa[p]&&rd>=2)cut[fa[p]]=true;//入度>=2且为根的结点,因为一棵树的根一删不管有几棵子树肯定都不连通了
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y);
        add(y,x);
    }
    memset(dfn,0,sizeof(dfn));
    for(int i=1;i<=n;i++) fa[i]=i;
    for(int i=1;i<=n;i++)
        if(!dfn[i]) tarjan(i);
    int ans=0;
    for(int i=1;i<=n;i++)
    if(cut[i])ans++;
    printf("%d\n",ans);
    for(int i=1;i<=n;i++)
    if(cut[i])printf("%d ",i);
    return 0;
}

题目描述

给出一个nn个点,mm条边的无向图,求图的割点。

输入输出格式

输入格式:

第一行输入n,mn,m

下面mm行每行输入x,yx,y表示xx到yy有一条边

输出格式:

第一行输出割点个数

第二行按照节点编号从小到大输出节点,用空格隔开

输入输出样例

输入样例#1: 复制

6 7
1 2
1 3
1 4
2 5
3 5
4 5
5 6

输出样例#1: 复制

1 
5

说明

对于全部数据,n \le 20000n≤20000,m \le 100000m≤100000

点的编号均大于00小于等于nn。

tarjan图不一定联通。

#include<bits/stdc++.h>
using namespace std;
int in,n,m,a,b,dfn[20005],low[20050],v[20005];
vector<int> G[20000];
void dfs(int u,int root){
    dfn[u]=low[u]=++in;
    int child=0;
    for(int i=0;i<G[u].size();i++){
        int w=G[u][i];
        if(!dfn[w]){
            dfs(w,root);
            low[u]=min(low[u],low[w]);
            if(u!=root&&low[w]>=dfn[u])v[u]=1;
            if(u==root)child++;
        }
        low[u]=min(low[u],dfn[w]);
    }
    if(u==root&&child>=2)v[root]=1;
}

int main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        cin>>a>>b;
        G[a].push_back(b);
        G[b].push_back(a);
    }
    int res=0;
    for(int i=1;i<=n;i++)if(!dfn[i]){dfs(i,i);}
    for(int i=1;i<=n;i++)if(v[i])res++;
    cout<<res<<endl;
    for(int i=1;i<=n;i++)if(v[i])cout<<i<<" ";
    return 0;
}

求割边和割点是有一点点不一样的,我们来看:

链接:https://ac.nowcoder.com/acm/contest/392/I
来源:牛客网
 

题目描述

月月和华华一起去逛公园了。公园很大,为了方便,可以抽象的看成一个N个点M条边的无向连通图(点是景点,边是道路)。公园唯一的入口在1号点,月月和华华要从这里出发,并打算参观所有的景点。因为他们感情很好,走多远都不会觉得无聊,所以所有景点和道路都可以无数次的重复经过。月月发现,有些路可走可不走,有些路则必须要走,否则就无法参观所有的景点。现在月月想知道,有几条路是不一定要经过的。因为这是个很正常的公园,所以没有重边和自环。

输入描述:

第一行两个正整数N和M,表示点数和边数。
接下来M行,每行两个正整数U和V表示一条无向边。
保证给定的图是连通的。

输出描述:

输出一行一个非负整数表示不一定要经过的边有几条。
#include<bits/stdc++.h>
using namespace std;
struct Node{int from,to;};
int id,low[100005],dfn[100005],n,m,a,b;
vector<int> G[100005];
vector<Node> res;
Node e;
void tarjin(int u,int dad){
    low[u]=dfn[u]=++id;
    for(int i=0;i<G[u].size();i++){
        int v=G[u][i];
        if(v==dad)continue;
        if(!dfn[v]){
            tarjin(v,u);//他的爸爸
            low[u]=min(low[u],low[v]);
            if(low[v]>dfn[u]){
                e.from=u;
                e.to=v;
                res.push_back(e);
            }
        }else low[u]=min(low[u],dfn[v]);//之前的时间戳
    }
}
int main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        cin>>a>>b;
        G[a].push_back(b);
        G[b].push_back(a);
    }

    for(int i=1;i<=n;i++){
        if(!dfn[i]){
            tarjin(i,i);
        }
    }
    cout<<m-res.size();
    return 0;
}

猜你喜欢

转载自blog.csdn.net/xizi_ghq/article/details/88372277
今日推荐