图的连通性算法


比赛链接

概念

割点:无向连通图,删除这个点和这个点关联的边,图不连通

点双连通图:无向连通图,没有割点出现

:无向连通图,删除某条边,图不连通

边双连通图:无向连通图,没有桥

时间戳:对一个图做深度优先搜索的时候,第一次访问某个点的时间

强连通分量:有向图任意两点都可互相到达


求割点和点连通分量

int times = 0;
int dfn[maxn], low[maxn]; // dfn记录时间戳
// low(u)来表示 u 以及其后代 **最多经过** 一条反向边能回到的最早的点的时间戳
int bcc_cnt = 0;  // 点双连通分量数量
bool iscut[maxn];  // 标记是否是割点
bool vis[maxn<<1]; // 记录这条边是不是访问过了处理重编
set<int> bcc[maxn];  // 记录每个点双连通分量里面的点
stack<edge> S; // 记录连通分量的点集
void dfs (int u, int fa) {
    dfn[u] = low[u] = ++times;
    int child = 0;  // 用来处理根结点子结点数
    for (int i = p[u]; i != -1; i = E[i].next) {
        int v = E[i].v;
        if(vis[i])continue;
        vis[i]=vis[i^1]=1;//
        if (dfn[v] == 0) {  // v 没有被访问过,u, v 是树边
            S.push(E[i]);
            ++child;
            dfs(v, u);
            low[u] = min(low[u], low[v]);
            /*if(low[v]>dfn[u]) 桥++;*/
            if (low[v] >= dfn[u]) {
            // 当且仅当 u 存在一个子结点 v,使得 v 及其所有后代都没有反向边连回 u 的**祖先**(不包括 u)
                iscut[u] = true;
                ++bcc_cnt;  // 增加一个点双连通分量
                while (true) {
                    edge x = S.top();
                    S.pop();
                    bcc[bcc_cnt].insert(x.u);
                    bcc[bcc_cnt].insert(x.v);
                    if (x.u == u && x.v == v) {
                        break;
                    }
                }
            }
        } else  {  // 反向边,注意 v == fa 的时候,是访问重复的边
            S.push(E[i]);
            low[u] = min(low[u], dfn[v]);// 初始的时候,low(u) = dfn(u)low(u)=dfn(u),我们认为自己当然可以回到自己
        }
    }
    if (fa < 0 && child == 1) {
        // fa < 0 表示根结点,之前根结点一定被标记为割点, 取消之
        //对于树根来说当且仅当它有两个或者更多的子结点的时候,它才是割点
        iscut[u] = false;
    }
}
//普通建图输出点霜
memset(dfn, 0, sizeof(dfn));
    times=bcc_cnt=0;
    dfs(1, -1);
    cout<<bcc_cnt<<endl;
    for(int i=1; i<=bcc_cnt; i++) {
        for(set<int>::iterator it=bcc[i].begin(); it!=bcc[i].end(); it++) {
            cout<<(*it)<<" ";
        }
        cout<<endl;
    }

在这里插入图片描述

emm
例题
铁路
一直t不知道为啥
55看队长的题解

桥和边连通分量

int times = 0;
int dfn[maxn], low[maxn];
int bcc_cnt = 0;        // 边双连通分量数量
vector<int> bcc[maxn];  // 记录每个点双连通分量里面的点 因为就经过一次就装在vector里
stack<int> S;
void dfs(int u, int fa) {
    dfn[u] = low[u] = ++times;
    S.push(u);
    for (int i = p[u]; i != -1; i = E[i].next) {
        int v = E[i].v;
        if (dfn[v] == 0) {  // v 没有被访问过,u, v 是树边
            dfs(v, u);
            low[u] = min(low[u], low[v]);// 根据low数组的含义
            
            /*if(low[v]>dfn[u]) 桥++;*/
            
        } else if (dfn[v] < dfn[u] && v != fa) {  // 反向边,注意 v == fa 的时候,是访问重复的边
            low[u] = min(low[u], dfn[v]);
        }
    }
    if (low[u] == dfn[u]) {  // 此时 u 是根结点或者 fa -> u 是桥
        ++bcc_cnt;           // 增加一个边双连通分量
        while (!S.empty()) {  //从栈中弹出 u 及 u 之后的顶点
            int x = S.top();
            S.pop();
            bcc[bcc_cnt].push_back(x);
            if (x == u) break;  
        }
    }
}
int main() {
    init();
    int n, m;
    cin >> n >> m;
    for (int i = 0; i < m; ++i) {
        int u, v;
        cin >> u >> v;
        insert(u, v);
        insert(v, u);
    }
    memset(dfn, 0, sizeof(dfn));
    times=bcc_cnt=0;
    dfs(1, -1);
    cout << bcc_cnt << endl;
    for (int i = 1; i <= bcc_cnt; ++i) {
        for (int j = 0; j < bcc[i].size(); j++) {
            cout << bcc[i][j] << " ";// 输出边双
        }
        cout << endl;
    }
    return 0;
}

例题
Redundant Paths
题意: 添加最少的边使这棵树上所有的链都变成环
缩点后是一棵树 贪心 注意重边标记

对于无向图的缩点,由于是无向图,所以要从u到v建一条边,又要从v到u建一条边,但是,在tarjan时会有两条边重复,这是一个麻烦,而且,还不得不建两条边,这该怎么办呢?
解决的方法就是,当同一条无向边的两条有向边的其中一条走过时,把另一条同时赋值为走过,这就要用到一个神奇的公式,^ 1。
举例来说,
0 ^ 1=1, 1 ^ 1=0;
2 ^ 1=3, 3 ^ 1=2;
4 ^ 1=5, 5 ^ 1=4;
而建边的时候,一条无向边的两条有向边刚好相差1

#include <cstring>
#include <iostream>
#include <stack>
#include <vector>
using namespace std;
const int maxm = 1010;  
const int maxn = 110; 
struct edge {
    int u, v;
    int next;
} E[maxm];
int p[maxn], eid = 0;
void init() {
    memset(p, -1, sizeof(p));
    eid = 0;
}
void insert(int u, int v) {
    E[eid].u = u;
    E[eid].v = v;
    E[eid].next = p[u];
    p[u] = eid++;
}
int times = 0;
int dfn[maxn], low[maxn];
int bc[maxm], in[maxn], chu[maxn];
int bcc_cnt = 0;
vector<int> bcc[maxn];
stack<int> S;
bool vis[maxm<<1];
void dfs(int u) {
    dfn[u] = low[u] = ++times;
    S.push(u);
    for (int i = p[u]; i != -1; i = E[i].next) {
		if(!vis[i]) {// 拿vis数组来标记是否经过 解决重边问题 然后缩点处理
			vis[i]=vis[i^1]=1;
			int v = E[i].v;
			if (dfn[v] == 0) {
				dfs(v);
				low[u] = min(low[u], low[v]);
			} else  {
				low[u] = min(low[u], dfn[v]);
			}
		}

    }
    if (low[u] == dfn[u]) {
        ++bcc_cnt;
        while (!S.empty()) {
            int x = S.top();
            S.pop();
            bc[x]=bcc_cnt; // 标记这个点是那个边双里的
            bcc[bcc_cnt].push_back(x);
            if (x == u) break;
        }
    }
}
int main() {
    init();
    int n, m;
    cin >> n >> m;
    for (int i = 0; i < m; ++i) {
        int u, v;
        cin >> u >> v;
        insert(u, v);
        insert(v, u);
    }
    memset(dfn, 0, sizeof(dfn));
    times=bcc_cnt=0;
    for (int i=1; i<=n; ++i) if (!dfn[i]) dfs(i);
    for(int u=1; u<=n; u++) {
		for(int i=p[u]; i!=-1; i=E[i].next) {
			int v=E[i].v;
			if(bc[v]!=bc[u]) {
				in[bc[v]]++;
			}
		}
    }int ans=0;
    for(int i=1; i<=bcc_cnt; i++) {
		if(in[i]==1) ans++;
    }
    cout<<(ans+1)/2<<endl;// 最后
    return 0;
}

强连通分量

复杂度(V+E)

#include <iostream>
#include <stack>
#include <set>
#include <cstring>
using namespace std;
const int maxm = 1010;
const int maxn = 110;
struct edge {
    int v;
    int next;
} E[maxm];
int p[maxn], eid = 0;
void init() {
    memset(p, -1, sizeof(p));
    eid = 0;
}
void insert(int u, int v) {
    E[eid].v = v;
    E[eid].next = p[u];
    p[u] = eid++;
}
int times = 0;
int dfn[maxn], low[maxn];
int scc_cnt = 0;  // 强连通分量数量
int sccno[maxn];  // 记录每个点属于的强连通分量的编号
set<int> scc[maxn];
stack<int> S;// 把点压入栈中 因为每个点只属于一个强连通分量
void dfs (int u) {
    dfn[u] = low[u] = ++times;
    S.push(u);
    for (int i = p[u]; i != -1; i = E[i].next) {
        int v = E[i].v;
        if (dfn[v] == 0) {  // v 没有被访问过,u, v 是树边
            dfs(v);
            low[u] = min(low[u], low[v]);
        } else if (!sccno[v]) {  // 对于已经求出 scc 的点,直接删除
            low[u] = min(low[u], dfn[v]);
        }
    }
    if (low[u] == dfn[u]) {  // 说明 u 是第一个被探测到的点
        ++scc_cnt;
        while (true) {
            int x = S.top();
            S.pop();
            sccno[x] = scc_cnt;// 记录这个点在哪个连通分量
            scc[scc_cnt].insert(x);
            if (x == u) {
                break;
            }
        }
    }
}
/*缩点
edge new_E[maxm];
int new_p[maxn], new_eid=0;
void new_init() {
    memset(new_p, -1, sizeof(new_p));
    new_eid=0;
}
void new_insert(int u, int v) {
    new_E[new_eid].v=v;
    new_E[new_eid].next=new_p[u];
    new_p[u]=new_eid++;
}*/
int main() {
    init();
    int n, m;
    cin >> n >> m;
    for (int i = 0; i < m; ++i) {
        int u, v;
        cin >> u >> v;
        insert(u, v);
    }
    memset(dfn, 0, sizeof(dfn));
    memset(sccno, 0, sizeof(sccno));
    times = scc_cnt = 0;
    for (int i = 1; i <= n; ++i) {
        if (!dfn[i]) {  // 每个点都要尝试 dfs 一次
            dfs(i);
        }
    }
 /* 缩点
 	new_init();
    for(int u=1; u<=n; ++u) {
        for(int i=p[u]; i!=-1; i=E[i].next) {
            int v=E[i].v;
            if(sccno[u]!=sccno[v]) {
                new_insert(sccno[u], sccno[v]);
            }
        }
    }
 */
    cout << scc_cnt << endl;
    for (int i = 1; i <= scc_cnt; ++i) {
        for (set<int>::iterator it = scc[i].begin(); it != scc[i].end(); ++it) {
            cout << (*it) << " ";// 输出连通分量
        }
        cout << endl;
    }
    return 0;
}

例题
迷宫
裸板子判断是不是强连通图
炸弹
本来想用并查集但是情况复杂的多
学校
题意:添加尽可能少的边使新图强连通:

我们先求出强连通分量,然后缩点得到一个DAG,假设新图上有a个结点的入度为0,有b个节点的出度为0,ans=max(a, b)(出度为0的点连入入度为0的点);
特殊情况:原图本身是强连通的时候答案是0不是1

欧拉回路

无序字母对

最小树形图

模板

冰激凌世界
网络


总结

bshd
看题面不知道怎么转化成图的连通性问题 板子也不熟

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值