连通性问题与Tarjan算法

连通性问题

这一周主要学习强化的是Tarjan算法,也算作是复习之后的博客,后面会继续更新的

无向图的连通性

如果一个点删除后会把一个连通图分成多个连通子图,则称此点为**割点,同理于边,称为**

  • 时间戳:dfs中第一次遍历到的时间戳

无向图的双连通分量(无向图)

无向图有两种双连通分量

  • 第一种是边的双连通分量
  • 第二种是点的双连通分量
E-DCC 边的双连通分量

桥:是一个无向边。对于一个无向连通图,如果删除某一条边会变得不连通,那么称这条边为桥

定义:极大的,不含有桥的连通区域被称为边的双连通分量

双连通分量性质
[1] 删去任意一条边仍然是连通图

​ [2] 任意两点之间一定包含两条不相交的路径

​ [3] 将一个无向图转化为边的双连通分量最小需要加的边的个数是
[ c n t + 1 2 ]    其 中 c n t 表 示 度 为 1 的 点 的 个 数 [\frac{cnt+1}{2}] \ \ 其中cnt表示度为1的点的个数 [2cnt+1]  cnt1
无向图中存在类似于有向图中的三种边:

[1]树枝边  (x,y)
[2]前向边  (a,b)
[3]后向边  (m,n)
E-DCC的缩点方法:
[1]类似于SCC,首先引入时间戳预处理出:
    dfn[x]:遍历到x节点的时间戳
    low[x]:x所能遍历到的最小的时间戳
[2]找到桥<=>找到dfn[x]<low[y]  //y在x下方,y无论如何也走不到x
[3]找到所有边的双连通分量有两种方法:
    1)将所有桥删除掉,剩下的每一个连通块都是一个连通分量
    2)类似于有向图,借助stack来判断dfn[x]==low[x]
找桥的方法:
void tarjan(int u,int from){
    dfn[u]=low[u]=++timestamp;
    
    for(int i=h[u];~i;i=ne[i]){
        int j=e[i];
        if(!dfn[j]){
            tarjan(j,i);  //from是反向边,此处为i
            low[u]=min(low[u],low[j]);
            
            if(dfn[u] < low[j]){   //如果满足桥的性质
                is_bridge[i]=is_bridge[i^1]=true;  //加边的时候是一偶一奇加的
            }
        }
        else if(i!=(from^1))  //如果不是反向边
            low[u]=min(low[u],dfn[j]);
    }
}
找到桥之后缩点
int scc_cnt, id[N];
void dfs(int u){
    id[u] = scc_cnt;
    for(int i=h[u];~i;i=ne[i]){
        int j=e[i];
        if(id[j] || bridge[i]) continue;
        dfs(j);
    }
}
int main(){
    rep(i,1,n) if(!id[i]) scc_cnt ++, dfs(i);
}
E-DCC的缩点 : 循环所有边
for(int i=0;i<idx;++i){
    int x = e[i], y = e[i^1];
    if(id[x] == id[y]) continue;
    add(id[x],id[y]);
}
V-DCC点的双连通分量

在这里插入图片描述

割点:在一个无向图中如果删去某一个点使得整个图变得不连通,则称此点为此无向图的割点

定义:极大的,不包含个点的连通块被称为点的双连通分量

性质:每一个割点至少属于两个连通分量

求割点:
    [1] 满足low[y]>=dfn[x]后需要分类讨论
    [2] 如果x不是根节点,那么x是割点
    [3] 如果x是根节点,则其至少有两个子节点yi都满足low[yi]>=dfn[x],此时x才能算割点

求点的双连通分量思路

[1] 记录时间戳,当前点入栈
[2] 特判,如果是孤立点就单独记录进对应连通块的数组
[3] 遍历所有邻边,并更新。如果没有更新过
    当找到了dfn[x]<=dfn[y]之后要对其讨论是否是割点:
        if(dfn[x]<=low[y]){
            cnt++;//对于记录当前有多少个分支+1  
            if(x!=root||cnt>1) x是割点
            将栈中元素弹出直至弹出y为止
            将x也放入当前双连通分量中
        }
V-DCC的缩点方式:
割点的判定:
void tarjan(int u){
    int cnt = 0;
    dfn[u] = low[u] = ++timestamp;
    
    for(int i=h[u];~i;i=ne[i]){
        int j = e[i];
        if(!dfn[j]){
            tarjan(j);
            low[u] = min(low[u], low[j]);
            
            if(dfn[u] <= low[j]) {
                cnt++;
                if(u!=root || cnt > 1)  cut[u] = true;
            }
        }
        else low[u] = min(low[u], dfn[j]);
    }
}
点的双连通分量求解

求V-DCC需要借助于栈来实现

void tarjan(int u){
    dfn[u] = low[u] = ++timestamp;
    stk[++ top] = u;
    
    if(u == root && h[u] == -1){ //孤立点
        dcc_cnt ++;
        dcc[dcc_cnt].push_back(u);
        return ;
    }
    
    int cnt = 0;
    for(int i=h[u];~i;i=ne[i]){
        int j = e[i];
        if(!dfn[j]){
            tarjan(j);
            low[u] = min(low[u], low[j]);
            
            if(dfn[u] <= low[j]) {
                cnt++;
                if(u!=root || cnt > 1)  cut[u] = true;
                dcc_cnt ++;
                int y;
                do{
                	y = stk[top --];
                    id[y] = dcc_cnt;
                    dcc[dcc_cnt].push_back(y);
                }while(y!=j);
                dcc[dcc_cnt].push_back(u);
            }
        }
        else low[u] = min(low[u], dfn[j]);
    }
}

注意当dfn[u] < dfn[j]的时候仍然要把u加入栈,因为此时x—y这个仍然是一个两个点的双连通分量

V-DCC缩点

在这里插入图片描述

int num = dcc_cnt;
rep(i,1,n) if(cut[i]) id[i] = ++num ;
rep(i,1,dcc_cnt){
    for(int j=0;j<dcc[i].size();++j){
        int u = dcc[i][j];
        if(cut[u]) add(h1,i,id[u]), add(h1,id[u],i);
        else from[u] = i; //记录当前u点属于哪个唯一的连通块
    }
}

电力

n个点m条边的无向图,求删除一个点后,连通块最多有多少即求一个割点使得删去此割点后的连通块的个数最多0

思路:先统计所有连通块的个数,然后枚举每一个连通块求割点的同时统计删除每个割点会形成多少个新的联通块 root=1 !root=2

//================================= 
const int N = 20010, M = 30010;
int n, m, k, timestamp, top, idx, scc_cnt, ans, root;
int e[M], ne[M], h[N], stk[M], dfn[N], low[N], deg[N];
bool ins[M], is_bridge[M];

void add(int a,int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void tarjan(int u){
    int cnt = 0;
    dfn[u] = low[u] = ++timestamp;
    
    for(int i=h[u];~i;i=ne[i]){
        int j = e[i];
        if(!dfn[j]){
            tarjan(j);
            low[u] = min(low[u], low[j]);
            
            if(dfn[u] <= low[j]) cnt++;
        }
        else low[u] = min(low[u], dfn[j]);
    }
    if(u!=root && cnt) cnt ++;
    ans = max(ans, cnt);
}
//=================================
int main(){
	n = read(), m = read();
	while(n || m){
	    int cnt = 0;timestamp = ans = idx = 0;
	    memset(h, -1, sizeof h);
	    memset(dfn,0,sizeof dfn);
	    memset(low,0,sizeof low);
	    
        rep(i,1,m){
    	    int a = read(), b = read();
    	    add(a, b), add(b, a); 
    	}
        for(root=0;root<n;root++) if(!dfn[root]) 
            tarjan(root), cnt ++;
        
        print(ans+cnt-1);
        
        n = read(), m = read();
    }
	return 0;
}

有向图的强连通分量SCC

  • 关于图上的传递性

定义:对于一个有向图,连通分量中的任意两点u,v;必有可以从u走到v,也可以从v走到u

强连通分量:极大连通分量,即不能再增加点使得其仍然是一个连通分量

作用:将一个有向图缩点成有向无环图(DAG)

在这里插入图片描述

将点分为4类:
    [1]树枝边  (x,y)
    [2]前向边  (a,b)
    [3]后向边  (m,n)
    [4]横叉边  (b,y)

问题:某一点是否在强连通分量中?

  • 情况1:存在后向边指向祖先节点
  • 情况2:先到横叉边,横叉边再通过后向边走到祖宗节点

step:

Tarjan算法求强连通分量(SCC)
对每个点定义两个时间戳
[1] dfn[u]表示遍历到u的时间戳
[2] low[u]表示从u开始走所能遍历到的最小的时间戳
[3] u是其所在的强连通分量的最高点,等价于dfn[u] == low[u]

在这里插入图片描述

Tarjan_scc模板 O(n+m):

void tarjan(int u){
    dfn[u]=low[u]=++timestamp;
    stk[++top]=u,in_stk[u]=true;
    
    for(int i=h[u];~i;i=ne[i]){
        int j=e[i];
        if(!dfn[j]){
            tarjan(j);
            low[u]=min(low[u],low[j]);    //u能到j,j能到的最小值u也一定能到
        }
        else if(in_stk[j]){     //栈中存储的所有点都不是其所在的强连通分量的最高点
            low[u]=min(low[u],dfn[j]);     //此时的j要么是祖先,要么是横叉点
        }
 	}
  	if(dfn[u]==low[u]){   //找到了强连通分量的最高点
    	int y;
        scc_cnt++;
        do{
       	    y=stk[top--];
            in_stk[y]=false;
            id[y]=scc_cnt;                                
        } while(y!=u);
    }
}

紧接着需要做缩点操作,用来建新图(DAG):

//缩点
for(int dot=1;dot<=n;dot++)
    for(int i=h[dot];~i;i=ne[i]){
        int j=e[i];
        if(id[i]!=id[j])  //如果i和j不在一个强连通分量中
            add(id[i],id[j]); //在两个强连通分量之间加一条边(建图)
    }

连通分量在缩完点之后就已经是满足拓扑序了,因此可以不用再写拓扑排序,联通分量编号递减的顺序就是拓扑序(证明由dfs找拓扑序的队列的逆序为拓扑序)

性质:1)将一个有向图转化为强连通分量所需要加的边的最小个数为
m i n { c n t 出 度 为 0 , c n t 入 度 为 0 } min\{cnt_{出度为0},cnt_{入度为0}\} min{cnt0,cnt0}

有向图的强连通分量算法2 Kosaraju算法

对原图进行一次dfs(任意起点)q

以第一次搜索出战时间的逆序对反图进行dfs

在这里插入图片描述

缩点的意义:

缩环成点,将有环图转化为DAG

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值