图的连通性

图的连通性

一、相关定义

1. 连通图

在无向图 GGG 中,若对于任意两点 xxxyyy 有路径,则称 xxxyyy 连通,图 GGG 一定为连通图;

2. 连通分量

无向图 GGG 的极大连通子图称为 GGG 的连通分量;

极大连通子图则为在图 G=(V,E)G = (V, E)G=(V,E) 中,对于连通子图 G′=(V′,E′),V′∈V,E′∈EG' = (V', E'), V' \in V, E' \in EG=(V,E),VV,EE ,不存在包含 G′G'G 的更大的 GGG 的连通子图 G′′=(V′′,E′′)G'' = (V'', E'')G′′=(V′′,E′′) ,其中 V′∈V′′,U′∈U′′V' \in V'', U' \in U''VV′′,UU′′

连通图只有一个连通分量,及其自身,而非连通的无向图有多个连通分量;

3. 强连通图

有向图 GGG 中,若对任意两点 xxxyyy ,存在从 xxxyyy 的路径以及从 yyyxxx 的路径,则称 GGG 是强连通图;

4. 强连通分量

有向图 GGG 的极大强连通子图称为 GGG 的强连通分量;

极大强连通子图则为在图 G=(V,E)G = (V, E)G=(V,E) 中,对于强连通子图 G′=(V′,E′),V′∈V,E′∈EG' = (V', E'), V' \in V, E' \in EG=(V,E),VV,EE ,不存在包含 G′G'G 的更大的 GGG 的强连通子图 G′′=(V′′,E′′)G'' = (V'', E'')G′′=(V′′,E′′) ,其中 V′∈V′′,U′∈U′′V' \in V'', U' \in U''VV′′,UU′′

强连通图只有一个强连通分量,及其自身,而非连通的有向图有多个强连通分量;

5. 割点

割点

无向连通图删除某点后,使整个图变为不连通的两部分的点;

割点集

所有割点的集合称为割点集;

点连通度

最小的割点集点数;

6. 割边 (桥)

割边 (桥)

无向连通图删除某边后,使整个图变为不连通的两部分的边;

割边集

所有割边的集合称为割边集;

边连通度

最小的割边集边数;

7. 双连通图

点双连通图

若无向图中删掉任意一个结点图仍联通,即不存在割点,则将此图称作点双连通图;

边双连通图

若无向图中删掉任意一条边图仍联通,即不存在桥,则将此图称作边双连通图;

8. 双连通分量

点双连通分量

无向图的每一个极大点双连通子图称作此无向图的点双连通分量 (v - DCC);

极大点连通双连通子图则为在图 G=(V,E)G = (V, E)G=(V,E) 中,对于点双连通子图 G′=(V′,E′),V′∈V,E′∈EG' = (V', E'), V' \in V, E' \in EG=(V,E),VV,EE ,不存在包含 G′G'G 的更大的 GGG 的点双连通子图 G′′=(V′′,E′′)G'' = (V'', E'')G′′=(V′′,E′′) ,其中 V′∈V′′,U′∈U′′V' \in V'', U' \in U''VV′′,UU′′

边双连通分量

无向图的每一个极大边双连通子图称作此无向图的边双连通分量 (e - DCC);

极大边连通双连通子图则为在图 G=(V,E)G = (V, E)G=(V,E) 中,对于边双连通子图 G′=(V′,E′),V′∈V,E′∈EG' = (V', E'), V' \in V, E' \in EG=(V,E),VV,EE ,不存在包含 G′G'G 的更大的 GGG 的边双连通子图 G′′=(V′′,E′′)G'' = (V'', E'')G′′=(V′′,E′′) ,其中 V′∈V′′,U′∈U′′V' \in V'', U' \in U''VV′′,UU′′

区别与联系
  1. 均基于无向图;

  2. 点双联通图为删点后联通性不变,而边双连通分量为删边后联通性不变;

  3. 点双连通分量一定为边双连通分量,反之不一定;

    可将删点看作删除与该点相连的边,由于图删除点后仍联通,则图删除点与点相连的边后仍联通,则图为边双连通分量;但若图为边双连通分量,则为只删一条边图仍联通,但不能保证删除所有与点相连通的边后图仍连通;

  4. 点双连通分量可以有公共点,但边双连通分量不能有公共边;

    若边双连通分量有公共边,则其不再为极大子图;

二、连通性判断

1. 判断无向图的联通性
思路

由于若有边 (x,y)(x, y)(x,y) ,则所有与 xxx 连通的节点均可通过此边与 yyy 连通,所以使用并查集,对于每条边,将边两顶点的连通集合合并,最终统计并查集根结点的数量,可得到连通分量的个数;

代码
int n, m, father[MAXN], tot;
bool flag[MAXN];
void firstset(int n) {
    for (int i = 1; i <= n; i++) {
        father[i] = i;
    }
    return;
}
int findset(int x) {
    if (father[x] == x) return x;
    return father[x] = findset(father[x]);
}
void push(int x, int y) {
    int a = findset(x), b = findset(y);
    if (a != b) father[a] = b;
    return;
}
int main() {
    scanf("%d %d", &n, &m);
    firstset(n);
    for (int i = 1; i <= m; i++) {
        int x, y;
        scanf("%d %d", &x, &y);
        push(x, y); // 通过此边连通两点
    }
    for (int i = 1; i <= n; i++) {
        if (!flag[findset(i)]) { // 找不同的连通分量
            flag[findset(i)] = true;
            tot++;
        }
    }
    printf("%d", tot);
    return 0;
}
2. 判断图中任意两点连通性
思路

可通过 Floyd 算法判断图中两点是否连通,还可在判断的同时,进行传递闭包计算;

代码
void firstset(int n) {
    for (int k = 1; k <= n; k++) {
        for (int i = 1; i <= n; i++) {
            for (int j =1; j <= n; j++) {
                dp[i][j] |= dp[i][k] & dp[k][j]; // 传递闭包
            }
        }
    }
    return;
}
bool check (int x, int y) {
    return dp[x][y]; // 返回 x 与 y 的联通性
}
3. 判断图是否存在环
无向图

使用 DFS 遍历图,若在遍历过程中访问到已访问过节点则有环;

代码

bool dfs(int i) {
	colour[i] = true; // 该节点已被遍历
	for (int t = 0; t < g[i].size(); t++) { // 遍历子节点
		int v = g[i][t];
        fa[v] = i;
        if (fa[i] != v) { // 遍历节点不为父节点
		    if (colour[v] == true) return false; // 重复走到同一节点,有环
		    bool tot = dfs(v); // 继续搜索
		    if (tot == false) return false;
        }
	}
	return true;
}
有向图

若仍然使用无向图判环的方法,则由于可能有多条路径从一点到另一点,所以找不到环;

由于环可从环内一点经过环走回其本身;

则可在搜索时标记,当遇到从一点起搜索,还未回溯时,再次访问该节点,则有环;

将为访问节点标记为 0 ,将已经遍历完成的节点标记为 2 ,将已访问但未回溯的节点标记为 1 ;

对图进行 DFS 遍历,若重复访问标记为 1 的节点,则说明原图有环;

代码,

bool dfs(int i) {
	colour[i] = 1; // 染色为已访问但未回溯
	for (int t = 0; t < g[i].size(); t++) { // 遍历子节点
		int v = g[i][t];
		if (colour[v] == 1) return false; // 若子节点搜索时回到搜索路径上,则有环
		bool tot = dfs(v); // 继续搜索
		if (tot == false) return false;
	}
	colour[i] = 2; // 染色为完成搜索
	return true;
}

三、Tarjan 算法

1. Tarjan 算法

Tarjan 算法时间复杂度为 O(V+E)O(V + E)O(V+E)

该算法利用了 DFS 遍历图的过程所记录的 DFN 与追溯值,可用来计算强连通分量与割点,割边;

2. 时间戳

在图的深度优先遍历过程中,按照每个节点被遍历的时间顺序,依次标记所有节点,该标记称为时间戳;

3. 搜索树

在连通图中任选一个节点出发进行深度优先遍历,每个点只访问一次,所有经过的边构成一棵树,此树则为搜索树;

若图不连通,则将图的各个连通块的搜索树构成无向图的搜索森林;

4. 追溯值

1. 定义

low[x]low[x]low[x]xxx 子节点与 xxx 经过一条不在搜索树上的边 (反向边) 所能够到达的最小时间戳;

2. 求法

当以第一次访问一个节点 vvv 时, dfn[v]dfn[v]dfn[v]low[v]low[v]low[v] 相同;

然后遍历 vvv 的子节点,

  1. 当节点未被访问过,由于 vvv 的子节点可以通过反向边遍历到的 vvv 的祖先节点,又由于 vvv 可以通过其与子节点的边遍历到其子节点遍历到的节点,则 low[v]low[v]low[v] 可通过其子节点的 lowlowlow 值改变,所以 low[v]low[v]low[v] 为其子节点的 lowlowlow 值的最小值;
  2. 当节点已被访问过,则说明此边为一条反向边,由于 lowlowlow 为只经过一条反向边所到达的节点 的时间戳最小值,所以 low[v]low[v]low[v] 不能再向上沿反向边遍历,所以 low[v]low[v]low[v] 取其与当前节点的 dfndfndfn 值的最小值;

则对图进行 DFS 遍历,遍历 iii 时进行初始化 low[i]=dfn[i]low[i] = dfn[i]low[i]=dfn[i] ,当遍历 iii 的子节点 vvv 时,如果 vvv 没有被遍历过,则先遍历 vvv ,得到子节点的 lowlowlow 值,然后使 low[i]=min(low[i],low[v])low[i] = min(low[i], low[v])low[i]=min(low[i],low[v]) ,如果 vvv 被遍历过,则使 low[i]=min(low[i],dfn[v])low[i] = min(low[i], dfn[v])low[i]=min(low[i],dfn[v])

6. 代码

以邻接表存储图为例;

void tarjan(int i) {
	dfn[i] = low[i] = ++len; // 初始化
	for (int t = 0; t < g[i].size(); t++) { // 遍历子节点
		int v = g[i][t];
		if (!dfn[v]) tarjan(v), low[i] = min(low[i], low[v]); // v 未被遍历过
		else low[i] = min(low[i], dfn[v]); // v 已被遍历过
	}
	retvrn;
}

四、强连通分量

1. 思路

如果某个节点 vvvlow[v]==dfn[n]low[v] == dfn[n]low[v]==dfn[n] ,表明该节点的 lowlowlow 值无法通过其父节点或孩子节点改变,则表明该节点不存在能够到达该节点的祖先节点的反向边,则该节点一定在一个由其自己与其的子孙节点组成的强连通分量中;

2. 实现

则对图进行 tarjan 算法,在遍历的同时,用一个栈存储遍历过的节点,

在遍历 iii 的子节点 vvv 时,如果 vvv 没有被遍历过,则先遍历 vvv ,得到子节点的 lowlowlow 值,然后使 low[i]=min(low[i],low[v])low[i] = min(low[i], low[v])low[i]=min(low[i],low[v])

vvv 已经遍历过,

  • vvv 属于某个强连通分量,说明此边为 iiivvv 之间的一条交叉边,不为当前所求以 iii 为根的强连通分量的边,所以不应对 vvvlowlowlow 值产生影响;

  • vvv 不属于任何强连通分量时,表明此边为 iiivvv 之间的环的一部分,则可用 dfn[v]dfn[v]dfn[v] 更新 low[i]low[i]low[i]

若计算完 low[i]low[i]low[i] 后,得到 low[i]==dfn[i]low[i] == dfn[i]low[i]==dfn[i] ,则说明搜索栈中直到上一次访问到 iii 点之间的所有节点均属于以 iii 为根的强连通分量;

3. 代码

void tarjan(int i) {
	dfn[i] = low[i] = ++len;
	s.push(i);
	for (int t = 0; t < g[i].size(); t++) {
		int v = g[i][t];
		if (!dfn[v]) tarjan(v), low[i] = min(low[i], low[v]);
		else if (!sccno[v]) low[i] = min(low[i], dfn[v]); // 当 v 不在任何强连通分量时,才用其的值更新 low[i]
	}
	if (dfn[i] == low[i]) { // i 为一个强连通分量的跟
		tot++; // 数量
		while (1) {
			int t = s.top();
			s.pop();
			sccno[t] = tot; // 将节点放入强连通分量中
			if (t == i) break;
		}
	}
	return;
}
// 主程序
for (int i = 1; i <= n; i++) {
    if (!sccno[i]) { // 使每个节点均有所属的强连通分量
        tarjan(i);
    }
}

4. 缩点

思路

枚举每一条边,若边上的两端点属于不同的强连通分量,则说明可以通过此边将两个强连通分量连接;

代码
vector <int> g1[MAXN]; // 存储缩点后的图
void resetpoint() {
    for (int i = 1; i <= n; i++) {
		for (int t = 0; t < g[i].size(); t++) {
			int v = g[i][t];
			if (sccno[i] != sccno[v]) { // 若两点不在同一个强连通分量
				g1[sccno[i]].push_back(sccno[v]); // 重新建边
			}
		}
	}
    return;
}

5. 构造强连通图

描述

给出一个有向图,求增加最少的边使得原图成为强连通图;

思路

由于强连通分量中的节点互相连通,则只需使各个强连通分量连通即可;

先求出图中所有的强连通分量,然后对原图进行缩点,得到一个 DAG 图,若缩点后的图有 tot1tot1tot1 个入度为 0 的强连通分量,有 tot2tot2tot2 个出度为 0 的强连通分量;

若一个节点的入度为 0 ,则此点是不能从其父节点向下遍历到的;如果一个节点的出度为 0 ,则此点是不能从子节点向上遍历到的;

则只要在每组出度为 0 的点和入度为 0 的点之间连一条的边,即可使图强连通;

则添加的边数为 max(tot1,tot2)max(tot1, tot2)max(tot1,tot2)

代码
#include <cstdio>
#include <vector>
#include <cstring>
#include <stack>
#include <algorithm>
#define MAXN 10005
using namespace std;
int n, m, dfn[MAXN], low[MAXN], len, sccno[MAXN], tot;
vector <int> g[MAXN], g1[MAXN];
stack <int> s;
bool in[MAXN], out[MAXN];
void tarjan(int i) {
	dfn[i] = low[i] = ++len;
	s.push(i);
	for (int t = 0; t < g[i].size(); t++) {
		int v = g[i][t];
		if (!dfn[v]) tarjan(v), low[i] = min(low[i], low[v]);
		else if (!sccno[v]) low[i] = min(low[i], dfn[v]); // 当 v 不在任何强连通分量时,才用其的值更新 low[i]
	}
	if (dfn[i] == low[i]) { // i 为一个强连通分量的跟
		tot++; // 数量
		while (1) {
			int t = s.top();
			s.pop();
			sccno[t] = tot; // 将节点放入强连通分量中
			if (t == i) break;
		}
	}
	return;
}
void resetpoint() {
    for (int i = 1; i <= n; i++) {
		for (int t = 0; t < g[i].size(); t++) {
			int v = g[i][t];
			if (sccno[i] != sccno[v]) { // 若两点不在同一个强连通分量
				g[sccno[i]].push_back(sccno[v]); // 重新建边
                out[sccno[i]] = in[sccno[v]] = true; // 统计入/出度
			}
		}
	}
    return;
}
int num() {
    int tot1 = 0, tot2 = 0; 
    for (int i = 1; i <= tot; i++) {
        if (in[i] == false) tot1++; // 计算入/出度为 0 的强连通分量个数
        if (out[i] == false) tot2++;
    }
    return max(tot1, tot2);
}
int main() {
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= m; i++) {
        int x, y;
        scanf("%d %d", &x, &y);
        g[x].push_back(y);
    }
    for (int i = 1; i <= n; i++) {
        if (!sccno[i]) {
            tarjan(i);
        }
    }
    resetpoint();
    printf("%d", num());
	return 0;
}

五、割点

1. 思路

当图进行 DFS 遍历时,会得到一颗搜索树,

则对于 iii 为割点时,条件有且仅有以下 2 个;

  1. iii 是搜索树的顶点,且 iii 在搜索树中至少有 2 个子节点;

    删除 iii 后, iii 在搜索树中的各个子节点不连通,所以 iii 为割点;

  2. iii 不为搜索树顶点,且对于 iii 的子节点 vvv 有, low[v]>=dfn[i]low[v] >= dfn[i]low[v]>=dfn[i]

    对于 low[v]>=dfn[i]low[v] >= dfn[i]low[v]>=dfn[i] 时,即 vvv 通过其子节点或一条反向边只能走到 iii 的子节点及其自身,不能到达 iii 的祖先节点,所以当把 iii 点删除后, vvv 的子节点无法与 iii 的祖先节点联通,所以 iii 为割点;

2. 代码

void tarjan(int i) {
	dfn[i] = low[i] = ++len; // 初始化
	int tot = 0;
	for (int t = 0; t < g[i].size(); t++) { // 遍历子节点
		int v = g[i][t];
		if (!dfn[v]) {
			tot++; // 记录搜索树中根结点的子树数量
			tarjan(v); // 计算节点 v low 值 
			low[i] = min(low[i], low[v]); // 更新 low 值
			if ((i == root && tot > 1) || (i != root && low[v] >= dfn[i])) { // 判断是否为割点
				cnt[i] = true; // 进行标记
			}
		} else {
            low[i] = min(low[i], dfn[v]); // 更新 low 值
        }
	}
	return;
}
// 主程序
for (int i = 1; i <= n; i++) {
    if (!dfn[i]) { // 所有点均遍历到
        len = 0; // 注意初始化
        root = i; // 根结点开始搜索
        tarjan(i);
    }
}

六、桥

1. 思路

对于无向图中一条边 (i,v)(i, v)(i,v) 为桥时,当且仅当

  • low[v]>dfn[i]low[v] > dfn[i]low[v]>dfn[i]

vvv 通过其子节点或一条反向边只能走到 iii 的子节点,不能到达 iii 的祖先节点以其本身,所以当把 (i,v)(i, v)(i,v) 删除后, vvv 的子节点无法与 iii 的祖先节点及其自身联通,所以 (i,v)(i, v)(i,v) 为割边;

注意,在进行 tarjan 算法时,应判断将要搜索的边是否为当前边的反向边,若是,则不应更新其的 lowlowlow 值,否则则更新;

对于桥的标记,由于是无向图,需要双向建边,所以当找到桥时,要将两向的边标记,所以可用链式前向星存图,双向边存在相邻位上,再利用 ^ 的性质,对于 iii ^ 1,若 iii 为奇数,则 iii ^ 1 等于其 i+1i + 1i+1 ;若 iii 为偶数,则 iii ^ 1 等于其 i−1i - 1i1 ,则当得到 ttt 为桥时,则 ttt ^ 1 也为桥;

则为对于枚举边寻找桥时,应只找编号为奇数的边;

2. 代码

void add(int x, int y) { // 建边
	ver[++tot] = y, net[tot] = head[x], head[x] = tot;
	return;
}
void tarjan(int i, int e) {
	dfn[i] = low[i] = ++len; // 初始化
	for (int t = head[i]; t; t = net[t]) {
		int v = ver[t];
		if (!dfn[v]) {
			tarjan(v, t); // 遍历子节点
			low[i] = min(low[i], low[v]);
            if (low[v] > dfn[i]) { // 满足为桥的条件
            	bridge[t] = bridge[t ^ 1] = true; // 标记桥
            }
		} else if (t != (e ^ 1)) { // 若不为当前边反向边
	        low[i] = min(low[i], dfn[v]);
	    }
	}
	return;
}
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); // 双向相邻建边
    }
    for (int i = 1; i <= n; i++) {
        if (!dfn[i]) {
            tarjan(i, 0);
        }
    }
    for (int i = 1; i <= tot; i += 2) { // 只枚举奇数号边
        if (bridge[i]) ans++;
    }
    printf("%d\n", ans);
    return 0 ;
}

七、双连通分量

1. 点双连通分量

思路

由于点双连通分量之间由割点连接,则对于一个 e-dcc 最先遍历到的一定为搜索树根节点或割点;

所以对图进行 DFS 遍历后,割点的搜索子树即为一个 v-dcc ;

但由于割点为 DFS 遍历完后从下向上回溯时寻找,所以包含树根的 v-dcc 不会被计算到,所以对于搜索树树根无论其子树大小,也应将其看作一个 v-dcc 的根;

则求法为,

  1. 先进行 tarjan 算法求割点,用栈记录遍历的节点;
  2. 每找到一个割点或树根,将其搜索树子树上的节点弹出栈,点集则为一个 v-dcc ,再将当前割点放入栈继续搜索;
代码
void tarjan(int i) {
	dfn[i] = low[i] = ++len; // 初始化
    s.push(i); // 使用栈存储遍历过的节点
	int tot1 = 0;
	for (int t = 0; t < g[i].size(); t++) { // 遍历子节点
		int v = g[i][t];
		if (!dfn[v]) {
			tot1++; // 记录搜索树中根结点的子树数量
			tarjan(v); // 计算节点 v low 值 
			low[i] = min(low[i], low[v]); // 更新 low 值
			if (low[v] >= dfn[i]) { // 判断是否为割点,但不需要判断是否为搜索树根结点
				if ((i != root) || (i == root && tot1 > 1)) cnt[i] = true; // 存储割点用于缩点
                tot++; // 点双连通分量数量
                while (1) {
                    int x = s.top();
                    s.pop();
                    dcc[tot].push_back(x); // 放入点双连通分量
                    if (v == x) break;
                }
                dcc[tot].push_back(i); // 放入割点
			}
		} else {
            low[i] = min(low[i], dfn[v]); // 更新 low 值
        }
	}
	return;
}
// 主程序
for (int i = 1; i <= n; i++) {
    if (!dfn[i]) { // 所有点均遍历到
        len = 0; // 注意初始化
        root = i; // 根结点开始搜索
        tarjan(i);
    }
}
缩点
思路

由于割点可能同时存在多个点双连通分量中,所以将每个割点与每个 v-DCC 作为缩点后的节点建图;

则先对所有的割点从 v-dcc 的个数开始重新编号,作为新图的节点编号,然后枚举每个 v-dcc 若当前 v-dcc 中有割点,则将割点与当前 v-dcc 建边,得到图;

代码
void resetpiont() {
	int num = tot; // 缩点新编号,从 v-dcc 个数开始
	for (int i = 1; i <= n; i++) {
		if (cnt[i] == true) {
			id[i] = ++num;
		}
	}
	for (int i = 1; i <= tot; i++) { // 枚举每个 e-dcc
		for (int j = 0; j < dcc[i].size(); j++) {
			int v = g[i][j];
			if (cnt[v]) { // 当前节点为割点
				g1[i].push_back(id[v]); // 建边
				g1[id[v]].push_back(i);
			} else {
				c[v] = i; // 记录节点 i 所在 v-dcc
			}
		}
	}
}

2. 边双连通分量

思路

由于各个 e-dcc 由桥边连接,又由于桥边不会属于任何一个 e-dcc ,所以只需计算出桥,然后将桥删除,剩余部分即为 e-dcc ;

则求法为,

  1. 先进行 tarjan 算法计算桥;
  2. 删除桥;
  3. 剩余部分为边双连通分量;
代码
void tarjan(int i, int e) {
	dfn[i] = low[i] = ++len; // 初始化
	for (int t = head[i]; t; t = net[t]) {
		int v = ver[t];
		if (!dfn[v]) {
			tarjan(v, t); // 遍历子节点
			low[i] = min(low[i], low[v]);
            if (low[v] > dfn[i]) { // 满足为桥的条件
            	bridge[t] = bridge[t ^ 1] = true; // 标记桥
            }
		} else if (t != (e ^ 1)) { // 若不为当前边反向边
	        low[i] = min(low[i], dfn[v]);
	    }
	}
	return;
}
void dfs(int i, int dcc) {
    c[i] = dcc; // i 属于 dcc 号 e-dcc
    for (int t = head[i]; t; t = net[t]) { // 继续遍历 i 子节点
        int v = ver[t];
        if (!c[v] && !bridge[t]) dfs(v, dcc); // 若 v 未被搜索过且当前边不为桥
    }
    return;
}
// 主程序
for (int i = 1; i <= n; i++) {
    if (!dfn[i]) {
        tarjan(i, 0); // 计算桥边
    }
}
for (int i = 1; i <= n; i++) {
    if (!c[i]) {
        tot++; // 当前 e-dcc 编号
        dfs(i, tot);
    }
}
缩点
思路

枚举每一条边,若边上的两端点属于不同的 e-dcc ,则说明可以通过此边将两个 e-dcc 连接,则建边;

由于两个 e-dcc 由桥连接,则说明两 e-dcc 没有其他的连接方式,则缩点后的 e-dcc 为一颗树;

代码
void resetpoint() {
    for (int i = 1; i <= e; i += 2) { // 枚举每一条边
        int x = ver[i], y = ver[i ^ 1];
        if (c[x] != c[y]) { // 若边上两点不在同一 e-dcc 则建边
            g1[c[x]].push_back(c[y]);
            g1[c[y]].push_back(c[x]);
        }
    }
}
构造边双连通图
思路

构造边双连通图,即增加边使所有的 e-dcc 连通,由于缩点后 e-dcc 为树形,所以增加的最少边则在树的叶子节点间增加,则若缩点后树叶子节点为 leafleafleaf 个,则最少增加边为 (leaf+1)/2(leaf + 1) / 2(leaf+1)/2 ;

注意,由于建边时不知道两顶点的顺序,所以建边时,两顶点入度均 + 1 ,最后入度为 1 的即为叶子节点;

代码
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
#define MAXN 500005
using namespace std;
int e = 1, ver[MAXN * 2], net[MAXN * 2], head[MAXN * 2];
int n, m, low[MAXN], dfn[MAXN], len, ans1, ans2, root, c[MAXN], tot = 0, leaf = 0, in[MAXN];;
bool bridge[MAXN];
void add(int x, int y) {
	ver[++e] = y, net[e] = head[x], head[x] = e;
	return;
}
void tarjan(int i, int e) {
	dfn[i] = low[i] = ++len; // 初始化
	for (int t = head[i]; t; t = net[t]) {
		int v = ver[t];
		if (!dfn[v]) {
			tarjan(v, t); // 遍历子节点
			low[i] = min(low[i], low[v]);
            if (low[v] > dfn[i]) { // 满足为桥的条件
            	bridge[t] = bridge[t ^ 1] = true; // 标记桥
            }
		} else if (t != (e ^ 1)) { // 若不为当前边反向边
	        low[i] = min(low[i], dfn[v]);
	    }
	}
	return;
}
void dfs(int i, int dcc) {
    c[i] = dcc; // i 属于 dcc 号 e-dcc
    for (int t = head[i]; t; t = net[t]) { // 继续遍历 i 子节点
        int v = ver[t];
        if (!c[v] && !bridge[t]) dfs(v, dcc); // 若 v 未被搜索过且当前边不为桥
    }
    return;
}
void resetpoint() {
    for (int i = 1; i <= e; i += 2) { // 枚举每一条边
        int x = ver[i ^ 1], y = ver[i];
        if (c[x] != c[y]) { // 若边上两点不在同一 e-dcc 则建边
            in[c[x]]++; // 统计节点入度,双节点都加
            in[c[y]]++;
        }
    }
}
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);
    }
    for (int i = 1; i <= n; i++) {
        if (!dfn[i]) {
            tarjan(i, 0); // 计算桥边
        }
    }
    for (int i = 1; i <= n; i++) {
        if (!c[i]) {
            tot++; // 当前 e-dcc 编号
            dfs(i, tot);
        }
    }
    resetpoint();
    for (int i = 1; i <= tot; i++) {
        if (in[i] == 1) { // 入度为 1 则为叶子节点
            leaf++;
        }
    }
    printf("%d", (leaf + 1) / 2);
    return 0 ;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值