双连通图强连通图概念解释以及tarjan算法求解该类问题总结

最近看了看类的相关题,感觉简单的题过于模板,但是对于难题的转化,如果对与这方面的概念不清楚,很难写,故总结一下。
PS:博客里部分内容会和离散数学中的图论知识有联系,如果没有了解过相关知识可能比较难理解。
下文所说的割点=关节点,割边=桥=关节边。


首先声明一下,名叫Tarjan的算法有三种,分别为
(1) 有向图的强联通分量类问题
(2) 无向图的双联通分量(求割点,桥)类问题
(3) 最近公共祖先(LCA)
这里前两类问题比较相似,而第三类是解决LCA公共祖先问题的算法,这篇帖子将不会涉及。


下面我来具体解释一下各种概念

首先,先来理解一下连通图的概念
连通图,顾名思义,他所有的点应该都是连通的,这里的连通对于有向图无向图来说还是有一定区别的。

无向图

在一个无向图G中,若从顶点i到顶点j有路径相连(当然从j到i也一定有路径),则称i和j是连通的。如果图中任意两点都是连通的,那么这个无向图就是连通图,简单来说只要他的所有节点都在一起连着,没有任何一个节点与其他节点直接没有连边,那么他就是一个连通图。

有向图

如果G是有向图,那么连接i和j的路径中所有的边都必须同向。如果图中任意两点都是连通的,则称为强连通图注意:有向图需要双向都有路径,如果i可以到j而j不可以到i,那么就不是强连通图)。
强连通图,就一定还会有弱连通图,将有向图的所有的有向边替换为无向边,所得到的图称为原图的基图。如果一个有向图的基图是连通图,则有向图是弱连通图
简单理解就是,一个有向图,如果不是强连通图,把所有的边从无向看成有向,把有向图看成无向图,这时他如果变成了连通图,那么就说他是弱连通图。

上面在讲解有向图和无向图时引出了两个概念,强连通图和连通图,我先从无向图中的连通图来开始介绍


在无向图连通图的问题中,有两个概念非常重要,割点和桥,在大多数问题中都需要求这两个东西,在讲这两个东西之前,先来给大家介绍几个前置概念。

点连通度与边连通度 by kuangbin

在一个无向连通图中,如果有一个顶点集合,删除这个顶点集合,以及这个集合中所有顶点相关联的边以后,原图变成多个连通块,就称这个点集为割点集合
一个图的点连通度的定义为,最小割点集合中的顶点数

类似的,如果有一个边集合,删除这个边集合以后,原图变成多个连通块,就称这个点集为割边集合
一个图的边连通度的定义为,最小割边集合中的边数
简单总结:
点双连通:删掉一个点之后,图仍联通
边双连通:删掉一条边之后,图仍联通

双连通

定义:在无向连通图中,如果删除该图的任何一个结点或边都不能改变该图的连通性,则该图为双连通的无向图。,和点连通度与边连通度来结合这来说,就是点连通度或边连通度大于1的图。
ps:这部分概念理解知道就好,对于做题来说帮助不是很大。

双连通图割点与桥 by kuangbin

如果一个无向连通图的点连通度大于 1,则称该图是点双连通的 (point biconnected),简称双连通或重连通。一个图有割点,当且仅当这个图的点连通度为 1,则割点集合的唯一元素
被称为割点 (cut point),又叫关节点 (articulation point)。

这里割点的概念就出来了,大概意思就是,如果连通图里有一个点,把这个点和他所连接的边删掉,那么他就不连通了,那么这个点就叫做割点,下图标红的点就是割点

双连通图强连通图概念解释以及tarjan算法求解该类问题总结_第1张图片(图源自网络,侵删)

理解了割点,那么理解割边也就很容易了

如果一个无向连通图边连通度大于 1,则称该图是边双连通的 (edge biconnected),简称
连通或重连通
一个图有桥,当且仅当这个图的边连通度为 1,则割边集合的唯一元素被称为 (bridge),又叫关节边 (articulation edge)。
下图标红的边就是割边
双连通图强连通图概念解释以及tarjan算法求解该类问题总结_第2张图片图源自网络,侵删)

可以看出,点双连通与边双连通都可以简称为双连通,它们之间是有着某种联系的,下文中提到的双连通,均既可指点双连通,又可指边双连通。

PS:而如果有桥,这个图必有割点,如果有割点,不一定会有桥,这个很好证明,看看图就知道了。

总结:若一个无向图中的去掉任意一个节点(一条边)都不会改变此图的连通性,即不存在割点(桥),则称作点(边)双连通图。

相信看过上面如此详细的概念,大家对割点和割边已经有了深刻的认识了,那么这里就给大家求割点和割边的两道入门题,学习如何用求tarjan算法求割点割边,可以在啊哈算法中看相应章节,个人感觉是市面上能找到资料里写的最好的了。
luogu P3388 【模板】割点(割顶)

#include 
#include
#include
#include
#include
using namespace std;
vector<int> g[20010];
int dfn[20010],low[20010],iscut[20010],son[20010];
int deep,root,n,m,ans;
int tarjan(int u,int fa)
{
    int child=0,lowu;
    lowu=dfn[u]=++deep;
    int sz=g[u].size();
    for(int i=0;i<sz;i++)
    {
        int v=g[u][i];
        if(!dfn[v])
        {
            child++;
            int lowv=tarjan(v,u);
            lowu=min(lowu,lowv);
            if(lowv>dfn[u])
            {
                iscut[u]=1;
            }
        }
        else
        {
            if(v!=fa&&dfn[v]<dfn[u])
            {
                lowu=min(lowu,dfn[v]);
            }
        } 
    }
    if(fa<0&&child==1)
    {
        iscut[u]=false;
    }
    low[u]=lowu;
    return lowu;
} 

int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++){
        int from,to;
        scanf("%d%d",&from,&to);
        g[from].push_back(to);
        g[to].push_back(from);
    }
    for(int i=1;i<=n;i++){
        if(!dfn[i]){
            root=i;
            tarjan(i,-1);
        }
    }
    for(int i=1;i<=n;i++){
        if(iscut[i]){
            ans++;
        }
    }
    printf("%d\n",ans);
    for(int i=1;i<=n;i++){
        if(iscut[i]){
            printf("%d ",i);
        }
    }
}

luogu P1656 炸铁路(割边)

#include 
#include
#include
#include
#include
using namespace std;

vector<pair<int,int>>bridge;
vector<int> g[10010];
int dfn[10010],low[10010];
int deep,root,n,m,ans;
bool cmp(pair<int,int> a,pair<int,int> b){
    if(a.first == b.first)
        return a.second < b.second;
    else
        return a.first < b.first;
}
int tarjan(int u,int fa)
{
    int lowu;
    lowu=dfn[u]=++deep;
    int sz=g[u].size();
    for(int i=0;i<sz;i++)
    {
        int v=g[u][i];
        if(!dfn[v])
        {
            int lowv=tarjan(v,u);
            lowu=min(lowu,lowv);
            if(lowv>dfn[u])
            {
                int from,to;
                from=u;
                to=v;
                if(from>to)
                {
                    swap(from,to);
                }
                bridge.push_back(make_pair(from,to));
            }
        }
        else
        {
            if(v!=fa&&dfn[v]<dfn[u])
            {
                lowu=min(lowu,dfn[v]);
            }
        } 
    }
    low[u]=lowu;
    return lowu;
} 

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int from,to;
        scanf("%d%d",&from,&to);
        g[from].push_back(to);
        g[to].push_back(from);
    }
    for(int i=1;i<=n;i++)
    {
        if(!dfn[i])
        {
            root=i;
            tarjan(i,-1);
        }
    }
    sort(bridge.begin(),bridge.end(),cmp);
    for(int i=0;i<bridge.size();i++)
    {
        printf("%d %d\n",bridge[i].first,bridge[i].second);
    }
}

好,现在这一阶段结束了,我们进行下一阶段的概念理解

双连通分量

先来看一看kuangbin的解释,个人感觉不是很好理解,所以下面还有一种比较简单易懂的解释。

在图 G 的所有子图 G’ 中,如果 G’ 是双连通的,则称 G’ 为双连通子图。如果一个双连通子
图 G’ 它不是任何一个双连通子图的真子集,则 G’ 为极大双连通子图。双连通分支
(biconnected component),或重连通分支,就是图的极大双连通子图。特殊的,点双连通分
支又叫做块。

双连通分量分为边双连通分量点双连通分量两种。
一个无向图中的每一个极大点(边)双连通子图称作此无向图的点(边)双连通分量

边双连通分量

边双连通分量:在分量内的任意两个点总可以找到两条边不相同的路径互相到达。总而言之就是一个圈,正着走反着走都可以相互到达,至少只有一个点。也就是同一边双内,点与点的边集中无桥,注意割边不属于任何一个边双联通分量。

简单的说,就是一个无向图,减去他们所有的割边,剩下个各个部分,就是各个边连通分量,如下两张图所示,红边为割边,删掉以后,剩下的三个子图就是三个边连通分量(图源网络,侵删)
双连通图强连通图概念解释以及tarjan算法求解该类问题总结_第3张图片
删掉之后
双连通图强连通图概念解释以及tarjan算法求解该类问题总结_第4张图片

边双联通分量求法模板

边双联通分量的求解关键点在于桥。如果遇到一个桥的时候就说明有两个边双联通分量。
poj 3177 Redundant Paths(边双连通分量+缩点) 来自这篇帖子

#include
#include
const int N=5000+5;
const int M=10000+5;

struct EDGE
{
    int v,next;
}edge[M*2];
int first[N],low[N],dfn[N],belong[N],degree[N],sta[M],instack[M];
int g,cnt,top,scc;
int min(int a,int b)
{
    return a<b?a:b;
}
void AddEdge(int u,int v)
{
    edge[g].v=v;
    edge[g].next=first[u];
    first[u]=g++;
}
void Tarjan(int u,int fa)
{
    int i,v;
    low[u]=dfn[u]=++cnt;
    sta[++top]=u;
    instack[u]=1;
    for(i=first[u];i!=-1;i=edge[i].next)
    {
        v=edge[i].v;
        if(i==(fa^1))
            continue;
        if(!dfn[v])
        {
            Tarjan(v,i);
            low[u]=min(low[u],low[v]);
        }
        else if(instack[v])
            low[u]=min(low[u],dfn[v]);
    }
    if(dfn[u]==low[u])
    {
        scc++;
        while(1)
        {
            v=sta[top--];
            instack[v]=0;
            belong[v]=scc;
            if(v==u)
                break;
        }
    }
}
int main()
{
    int n,m,u,v,i,j;
    scanf("%d%d",&n,&m);
        g=cnt=top=scc=0;
        memset(first,-1,sizeof(first));
        memset(low,0,sizeof(low));
        memset(dfn,0,sizeof(dfn));
        memset(instack,0,sizeof(instack));
        memset(degree,0,sizeof(degree));
        for(i=0;i<m;i++)
        {
            scanf("%d%d",&u,&v);
            {
                AddEdge(u,v);
                AddEdge(v,u);
            }
        }
        for(i=1;i<=n;i++)
            if(!dfn[i])
                Tarjan(1,-1);
        for(i=1;i<=n;i++)
        {
            for(j=first[i];j!=-1;j=edge[j].next)
            {
                v=edge[j].v;
                if(belong[i]!=belong[v])
                    degree[belong[i]]++;
            }
        }
        int sum=0;
        for(i=1;i<=n;i++)
            if(degree[i]==1)
                sum++;
        int ans=(sum+1)/2;
        printf("%d\n",ans);
    return 0;
}

点连通双分量

点双连通分量至少为一个点。对于同属一个点双的任意点,删除后,该分量中的点仍能互相到达;或者说仅对于该分量而言,无割点。
由于割点将原图分成互不相连的多个联通块,显然每个联通块本身已经是一个点双了,但不完整,因为相邻的割点在边界,如果与联通块共同组成一个新联通块,割掉后也不会另外产生联通块(相当于该连通块上的叶子节点),所以需要加上割点来才完整(故割点会同时出现在多个点双联通分量中)。但注意,非割点只会出现在一个点连通分量中(图源网络,侵删)

原图
双连通图强连通图概念解释以及tarjan算法求解该类问题总结_第5张图片
其中一个点双连分量双连通图强连通图概念解释以及tarjan算法求解该类问题总结_第6张图片
与上面那个点连通分量同割点的另一个点连通分量
双连通图强连通图概念解释以及tarjan算法求解该类问题总结_第7张图片

点双联通分量的求解关键点在于割点。

点双联通分量求法模板

该模板可求,遇见题可根据相求的点进行求解。
点连通分量个数:num
每个点连通分量的个数:bccnum[i]
求解完点连通分量之后的结果:bcc

#include
using namespace std;
typedef long long LL;
const int mod = 998244353;//取模
const int maxn = 3e5+10;//点的数量
const int maxm = 10e5+10;//边的数量,注意双向边要弄两倍的
struct edge
{
    int to,pre;
}edges[maxm];//邻接表
int head[maxn],dfn[maxn],dfs_clock,tot;
LL num;//BCC数量 
int Stack[maxn],Top;//模拟栈 
vector<int>bcc[maxn];//缩点之后的图
int bccnum[maxn];//存每个合点中包含点的数量
inline int Min(int a,int b){
    if(a>b)return b;
    else return a;
}//注意min是自己写的
/* inline LL qpow(LL x) { */
/*     LL res = 1, temp = 2; */
/*     while (x) { */
/*         if (x & 1) { */
/*             res = (res * temp) % mod; */
/*         } */
/*         temp = (temp * temp) % mod; */
/*         x >>= 1; */
/*     } */
/*     return res % mod; */
/* } */
int tarjan(int u,int fa)
{
    int lowu=dfn[u]=++dfs_clock;
    for(int i=head[u];i;i=edges[i].pre)
        if(!dfn[edges[i].to])
        {
            Stack[++Top]=edges[i].to;//搜索到的点入栈 
            int lowv=tarjan(edges[i].to,u);
            lowu=Min(lowu,lowv);
            if(lowv>=dfn[u])//是割点或根 
            {
                num++;
                while(Stack[Top]!=edges[i].to){
                    /* Top--; */
                    bcc[num].push_back(Stack[Top--]);//图
                    bccnum[num]++;//点的加法
                }
                /* Top--; */
                bccnum[num]++;
                bccnum[num]++;

                bcc[num].push_back(Stack[Top--]);//目标点出栈 
                bcc[num].push_back(u);//不要忘了将当前点存入bcc 
            }
        }
        else if(edges[i].to!=fa)
            lowu=Min(lowu,dfn[edges[i].to]);
    return lowu;
}
inline void add(int x,int y)//邻接表存边 
{
    edges[++tot].to=y;
    edges[tot].pre=head[x];
    head[x]=tot;
}
int main()
{
    int n,m;
    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);
    }
    for(int i=1;i<=n;i++)//遍历n个点tarjan 
        if(!dfn[i])
        {
            Stack[Top=1]=i;
            tarjan(i,i);
        }
    for(int i=1;i<=num;i++)
    {
        printf("BCC#%d: \n",i);
        printf("该分量点的个数:%d\n",bccnum[i]);
        printf("分别包含哪一个点:\n");
        for(size_t j=0;j<bcc[i].size();j++){
            printf("%d ",bcc[i][j]);
        }
        printf("\n");
    }
    return 0;
}

【双连通分量常见的模型和问法】

1.给出的图是非连通图,如:
a.有一些点,一些边,加最少的边,要使得整个图变成双联通图。
大致方法:求出所有分量,把每个分量看成一个点,统计每个点的度,有一个度为一则cnt加1,答案为(cnt+1)/2;
b.有一些点,一些边,问最少多少个点单着。
大致方法:求出所有的分量即可,但要注意不同的题可能有特殊要求(如圆桌骑士要求奇圈,要用到二分图判定)
c.各种变式问题

2.给出的图是连通图,如:
a.给定一个起点一个终点,求各种问题是否能实现。
大致方法:求出所有分量,并把每个分量当成点,于是问题得到化简;
b.给一个图,然后有大量的离线回答。
大致方法:求出所有分量,再求出上下子树的信息;
c.各种变式问题;


无向图基本介绍完了,现在在来介绍介绍有向图的相关概念。

强连通图

就像上面说过的,强连通是指一个有向图中任意两点v1、v2间存在v1到v2的路径及v2到v1的路径,同样,有向图有强连通分量的这一说法。

强连通分量

强连通图的基础上加入一些点和路径,使得当前的图不在强连通,称原来的强连通的部分为强连通分量。 与上面两种类似,强连通分量也至少为一个点。

缩点

同属一个强连通分量的点,互相之间可以互相到达,所以他们在很多问题中可以看成是一个点,把整个图里每个强连通分量看成一个点的过程就叫做缩点,之后在建一个新图,该图就一定是有向无环图了,便可以解决很多在缩点之前难以解决的问题,建议可以去找板子题做一做。

强连通分量问题模板

我借这道题,把强连通分量几乎会出到的所有模板,都写在这道题的模板里面,做题时只需要选择自己需要的自行使用
tarjan求强连通分量并缩点建图+dag有向无环图的dp问题去求解最大值 洛谷p3387

#include 
#include 
#include 
#include 
#include 
#define repu(i,a,b) for(int i=a;i
using namespace std;
#define N 10005            /// 题目中可能的最大点数
stack<int>sta;            /// 存储已遍历的结点
vector<int>gra[N];        /// 邻接表表示图
int dfn[N];        /// 深度优先搜索访问次序
int low[N];        /// 能追溯到的最早的次序
int InStack[N];  // 检查是否在栈中(2:在栈中,1:已访问,且不在栈中,0:不在)
vector<int> Component[N]; /// 获得强连通分量结果
int InComponent[N];          /// 记录每个点在第几号强连通分量里
int Index,ComponentNumber;/// 索引号,强连通分量个数
int n,m;          // 点数,边数
int chu[N];         //出度
int dianquan[N];   //初始图点权
int dianquan2[N];  //缩点后点权
int dp[N];          //dag缩点时
int vis[N];
int ru[N];           //入度
int num[N]; //各个强连通分量包含点的个数,数组编号 1 ∼ scc
void init() //清空容器,数组
{

    memset(dp, 0, sizeof(dp));
    memset(vis, 0, sizeof(vis));
    memset(dianquan2, 0, sizeof(dianquan2));
    memset(dianquan ,0, sizeof(dianquan));
    memset(dfn, 0, sizeof(dfn));
    memset(low, 0, sizeof(low));
    memset(chu, 0, sizeof(chu));
    memset(InStack, 0, sizeof(InStack));
    Index = ComponentNumber = 0;
    for (int i = 1; i <= n; ++ i)
    {
        gra[i].clear();
        Component[i].clear();
    }
    while(!sta.empty())
        sta.pop();
}
void tarjan(int u)
{
    InStack[u] = 2;
    low[u] = dfn[u] = ++ Index;
    sta.push(u);///寻找u所在的强连通分量
    for (size_t i = 0; i < gra[u].size(); ++ i)
    {
        int t = gra[u][i];
        if (dfn[t] == 0)///不在的继续递归
        {
            tarjan(t);///递归到头了就
            low[u] = min(low[u], low[t]);
        }
        else if (InStack[t] == 2)///在栈里
        {
            low[u] = min(low[u], dfn[t]);
        }
    }
    if(low[u] == dfn[u])///sta出栈就是一个强连通分量的
    {
        ++ComponentNumber;///强连通分量个数
        while (!sta.empty())
        {
            int j = sta.top();
            sta.pop();
            InStack[j] = 1;///已访问但不在栈中
            InComponent[j]=ComponentNumber;
            dianquan2[ComponentNumber]+=dianquan[j];
            ///记录每个点在第几号强连通分量里
            num[ComponentNumber]++;
            if (j == u)
                break;
        }
    }
}
void input()
{
    int x,y;
    scanf("%d%d",&n,&m);

    repu(i,1,n+1)
    {
        scanf("%d",&dianquan[i]);
    }

    repu(i,1,m+1)
    {
        scanf("%d%d",&x,&y);        
        gra[x].push_back(y);///有向图才有强连通分量
    }
}

int dfs(int u){

    if(dp[u]){
        return dp[u];
    }
    else{
        dp[u] = dianquan2[u];
        for(size_t i = 0;i < Component[u].size();i++){
            int y = Component[u][i];
            dp[u] = max(dp[u],dfs(y)+dianquan2[u]);
        }
        return dp[u];
    }
}
void solve()
{
    for(int i=1; i<=n; i++)
        if(!dfn[i])
            tarjan(i);
    //缩点
    for(int i = 1;i <= n;i++){
        for(size_t j = 0;j < gra[i].size();j++){
            if(InComponent[i] != InComponent[gra[i][j]]){
                //chu[InComponent[i]]++;//求出度
                //ru[InComponent[gra[i][j]]]++;//求入度
                Component[InComponent[gra[i][j]]].push_back(InComponent[i]); //缩点建图
            }
        }
    }
    int aans = 0;
    for(int i = 1;i <= ComponentNumber;i++){
        memset(dp,0,sizeof(dp));
        aans = max(dfs(i),aans);
        /* printf("%d ",dianquan2[i]); */
    }
    printf("%d\n",aans);
}

int main()
{
    init();
    input();
    solve();
    return 0;
}

【强连通分量常见的模型和问法】

1.给出的是非连通图,如:
a.有一些点,一些有向边,求至少加多少边使任意两个点可相互到达
大致方法:求出所有的分量,缩点,分别求出出度入度为0的点的数量,取多的为答案;
b.有一些点,一些有向边,求在这个图上走一条路最多可以经过多少个点
大致方法:求出所有的分量,缩点,形成一个或多个DAG图,然后做DAG上的dp
c.有一些点,一些有向边,给出一些特殊点,求终点是特殊点的最长的一条路
大致方法:求出所有分量,并标记哪些分量有特殊点,然后也是DAG的dp

2.给出的是连通图,比较少,有也比较简单


总结:

1.遇到非连通图几乎可以肯定是要求连通分量,不论是无向还是有向图;(可以节约大量思考时间)
2.凡是对边、点的操作,在同一个分量内任意一个点效果相同的,几乎都是缩点解决问题;再粗暴点,几乎求了连通分量都要缩点;
3.一定要考虑特殊情况,如整个图是一个连通分量等。
4.对于双连通分量要分析是边还是点双连通分量;通过题目来判断;
5.拿到题目要先搞清楚给的是连通图还是非连通图。

这篇文章有部分是借鉴其他一些博客的内容,链接我放在下面,这篇帖子在算法具体实现上讲解的更加细化,更加适合入门看看。
基本图论-连通分量(强/弱联通 割点/边 边/点双)
tarjan求强连通分量+缩点+割点/割桥(点双/边双)以及一些证明

你可能感兴趣的:(ACM算法讲解)