图的连通性(25.10.13)

图的连通性

概述

  1. 首先有几个大的概念:
    极大:扩大到不能再扩大了,包含所有符合条件的内容

    强连通分量:指在某个子图内,任意两个点之间都有路径可以到达,这个子图在极大的时候,称为强连通分量

    双连通:指在两点之间,若选择任意一条边删去,但两点仍连通,称两点双连通

    边/点双连通分量:指在某个无向子图内,极大边/点双连通子图
    传递性:边双连通可传递,点双连通不可传递
    a——b,b——c为边双连通,a——c为边双连通
    a——b,b——c为点双连通,a——c不一定为点双连通

    割点:指删掉某个点可以使极大连通分量数量增加
    割边(桥):指删掉某个边可以使极大连通分量数量增加

  2. 针对这些需要一个算法:tarjan
    dfn记录dfs序(生成树)下此点的顺序
    low记录此点可以从非树边回到的最早dfn序节点

双连通分量

点双连通分量

本质是判断所有割点,之后按连通性做判断,使用栈进行操作,可以有效的去防止割点的延伸,把所有连通子图输出

#include<bits/stdc++.h>
using namespace std;
const int N = 5*1e5+10;
const int M = 5*1e6+1000;
int n,m;

int cnt=1, h[N];
struct node {
 int nex, to;
}e[M*2];
void add(int u, int v) {
 e[++cnt].nex = h[u];
 e[cnt].to = v;
 h[u] =  cnt++;
}

int dfn[N], low[N], cnt_dfn=0, root;
int vis[N];
// int c[N], num_c=0;

int stack_c[N], top=0, cnt_c=0;//多了一个栈的处理
vector<int> ans[M];
void tarjan(int u, int last) {//tarjan去求出dfn和low
 dfn[u] = low[u] = ++cnt_dfn;
 stack_c[++top] = u;//放点入栈
 if (u==root&&h[u]==0) {
     ans[++cnt_c].push_back(u);//如果是根节点并且无子边,说明是单点
     return;
 }
 int count = 0;
 for (int i=h[u]; i; i = e[i].nex) {
     int v = e[i].to;
     if (!dfn[v]) {
         tarjan(v,i);
         low[u] = min(low[u],low[v]);
         
         // 此时u是割点,v所在的子树与u构成一个新的V-DCC
         // 想法是u可能会多次应用,发现了一个割点就将其与v所在子树的所有存进去,之后断掉子树找新的
         // 因为是递归,所有的v的子树早就处理好了,而且是先把v下头的所有DCC处理了递归上来的
         if (low[v]>=dfn[u]) {//发现了新的断点,下头点没法回上头分量了
             count++;
             if (count>1||root!=u) {//对于根节点的操作
                 vis[u] = 1;
             }
             cnt_c++;
             int z;
             do {
                 z=stack_c[top--];//把栈挨着弹出
                 ans[cnt_c].push_back(z);
             }while (z!=v);// 循环直到弹出v(子树的起点)
             
             //割点加入V-DCC
             ans[cnt_c].push_back(u);
         }
     }else if (i!=(last^1)) {
         low[u] = min(low[u],dfn[v]);
     }
 }
}
int main() {
 cin>>n>>m;
 for (int i=1; i<=m; i++) {
     int u, v;
     cin>>u>>v;
     if (u==v) continue;
     add(u,v);
     add(v,u);
 }
 for (int i=1; i<=n; i++) {
     if (!dfn[i]) {
         root = i;
         tarjan(i,0);
     }
 }
 cout<<cnt_c<<"\n";
 for (int i=1; i<=cnt_c; i++) {
     cout<<ans[i].size();
     for (int j=0; j<ans[i].size(); j++) {
         cout<<" "<<ans[i][j];
     }
     cout<<"\n";
 }
 return 0;
}

边双连通分量

本质是判断所有割边,然后除了割边的所有连通分量统计记录

#include <bits/stdc++.h>
using namespace std;
const int N = 5*1e5+100;
const int M = 2*1e6+100;
int n,m;

vector<int> ed[N*2];
//链式前向星
int cnt=1, h[N];
struct node {
    int nex, to;
}e[M*2];
void add(int u, int v) {
    e[++cnt].to = v;
    e[cnt].nex = h[u];
    h[u] = cnt;
}

bool vis[M*2];
//tarjan
int dfn[N], low[N], cnt_dfn=0;//时间戳
// int d[N], dc, in_stack[N], dcc[N];//栈,指针,是否在栈,每个点所在的dcc的编号
void tarjan(int u, int fa) {//求割边,但是可以直接在这里处理,用个栈
    dfn[u] = low[u] = ++cnt_dfn;
    for (int i=h[u]; i; i=e[i].nex) {
        int v=e[i].to;
        if (!dfn[v]) {
            tarjan(v,i);
            low[u] = min(low[u],low[v]);
            if (dfn[u]<low[v]) {
                vis[i] = vis[(i^1)] = true;
            }
        }
        else if (i!=(fa^1))
            low[u] = min(low[u],dfn[v]);
    }
}

//dfs序
int c[N], dcc=0;
void dfs(int u) {
    c[u] = dcc;
    if (u)
        ed[dcc].push_back(u);
    for (int i=h[u]; i; i=e[i].nex) {
        int v=e[i].to;
        if (c[v]||vis[i]) continue;
        dfs(v);
    }
}
int main() {
    memset(low,0x3f, sizeof(low));
    cin>>n>>m;
    for (int i=1; i<=m; i++) {
        int u, v;
        cin>>u>>v;
        add(u, v);
        add(v, u);
    }
    for (int i=1; i<=n; i++)
        if (!dfn[i]) tarjan(i,0);
    for (int i=1; i<=n; i++)
        if (!c[i]) {
            dcc++;
            dfs(i);
        }
    cout<<dcc<<endl;
    for (int i=1; i<=dcc; i++) {
        cout<<ed[i].size()<<" ";
        for (int j=0; j<ed[i].size(); j++)
            cout<<ed[i][j]<<" ";
        cout<<endl;
    }
    return 0;
}

思考方向

在看到一个图之后,

posted @ 2025-10-13 19:42  Yuriha  阅读(0)  评论(0)    收藏  举报