图论3:连通性问题(复杂度均为 O(N + M) )

目录

二、强连通分量相关

(一)Tarjan求强连通分量

(二)缩点

三、双连通分量相关

(一)点双连通分量相关

[一]割点

[二]点双连通分量

(二)边双连通分量相关

[一]割边/桥

[二]边双连通分量


1.思路:(1)意思就是前辈放在前面

(2)先找到入度为0 的点,这样就没有是它前辈 的点,它就可以放在最前面

(3)放进去后,它的后辈入度减去1 ,继续找为0的重复上述步骤 即可;

(4)如果放进去的总长度小于点的总数 ,则没有合法 的拓扑序,因为这就说明至少某一次找不到为0的 ,存在互相依赖关系,无法排先后。

2.代码如下:

cpp 复制代码
#include <bits/stdc++.h>
#define int long long
using namespace std;

int N, a, cnt = 0, into[110], ans[110];
vector<int> g[110];

void topu() {
    queue<int> q;
    for (int i = 1; i <= N; i++) {
        if (!into[i])
            q.push(i);
    }
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        ans[++cnt] = u;
        for (int v : g[u]) {
            into[v]--;
            if (!into[v])
                q.push(v);
        }
    }
}

signed main() {
    scanf("%lld", &N);
    for (int i = 1; i <= N; i++) {
        for (int j = 1; ; j++) {
            scanf("%lld", &a);
            if (!a)
                break;
            g[i].push_back(a);
            into[a]++;
        }
    }
    topu();
    //if (cnt < N)
    //    printf("No");
    for (int i = 1; i <= N; i++) {
        printf("%lld ", ans[i]);
    }
}

二、强连通分量相关

(一)Tarjan求强连通分量

1.一些概念:(1)树边:DFS时 走的边;

(2)返祖边:原有向图中 从DFS生成树的后代指向祖先 的边;

(3)前向边:与返祖边相反

(4)横叉边:原有向图中 在DFS生成树中指向非直系亲属 的边;

(5)强连通:两个点互相可达

(6)强连通子图:一个有向子图内,所有点两两之间均强连通

(7)分量:极大子图,满足要求且不能再加入任何一个点。

2.一些结论:(1)强连通分量是它是一棵树充分不必要 条件;

(2)所有强连通分量互不交叉 ,因为如果交叉那么它们可以形成一个更大的强连通分量

3.思路:(1)维护两个数组,dfn存DFS序 ,low存u能访问到的最早的已经在栈里 的时间戳;

(2)访问到之后更新dfn[u]和low[u]为当前时间戳 ,因为刚刚访问到这棵子树所以最早的就是u本身 ,节点入栈

(3)之后进行循环 :如果没有访问过v递归 并更新low[u]=min(low[u],low[v]) ;否则如果访问过而且在栈里面,更新low[u]=min(low[u],dfn[v]或low[v]) ;

(4)之后如果dfn[u]=low[u] ,则形成了一个强连通分量,所有点出栈+记录

4.问题:(1)为什么如果访问过但是不在栈里 不进行处理?

(2)为什么dfn[u]=low[u]则形成了一个强连通分量?

(3)为什么还在栈里面的一定可以到达u?

(4)为什么low[u]可以更新为low[v]?

解答:(1)访问过不在栈里说明出栈了 ,出栈了说明该点已经形成强连通分量了

(2)必要 性:形成强连通分量说明根节点u无法回溯 ,即dfn[u]=low[u];充分 性:遍历完之后dfn[u]=low[u],说明u无法回溯至少它自己是强连通分量 ,而栈里的都可以到达u,u也可以到达它们,那么它们彼此互相强连通 ,因此整个是强连通子图 ,而如果不是强连通分量,那么会存在一个点与之强连通,这样的点一定不在其他分量中,一定在栈里,与假设矛盾,因此是强连通分量

(3)如果到不了u,那么v和u不强连通 ,已经事先出栈 了;

(4)因为既然u可以到达v,那么v可到达的点u也可以到达

5.代码如下:

cpp 复制代码
#include <bits/stdc++.h>
#define int long long
using namespace std;

bool in_stack[10010], vis[10010];
int n, m, u, v, dfn[10010], low[10010], scc[10010], timer = 0, sc = 0;
stack<int> stk;
vector<int> g[10010];
vector<int> ans[10010];

void tarjan(int u) {
	dfn[u] = low[u] = ++timer;
	stk.push(u);
	in_stack[u] = 1;
	for (int v : g[u]) {
		if (!dfn[v]) {
			tarjan(v);
			low[u] = min(low[u], low[v]);
		} else if (in_stack[v]) {
			low[u] = min(low[u], dfn[v]);
		}
	}
	if (dfn[u] == low[u]) {
		sc++;
		while (1) {
			int v = stk.top();
			stk.pop();
			in_stack[v] = 0;
			scc[v] = sc;
			if (v == u)
				break;
		}
	}
}

signed main() {
	scanf("%lld%lld", &n, &m);
	for (int i = 1; i <= m; i++) {
		scanf("%lld%lld", &u, &v);
		g[u].push_back(v);
	}
	for (int i = 1; i <= n; i++) {
		if (!dfn[i])
			tarjan(i);
	}
	for (int i = 1; i <= n; i++) {
		ans[scc[i]].push_back(i);
	}
	printf("%lld\n", sc);
	for (int i = 1; i <= n; i++) {
		int t = scc[i];
		if (vis[t])
			continue;
		vis[t] = 1;
		for (int j = 0; j < ans[t].size(); j++)
			printf("%lld ", ans[t][j]);
		printf("\n");
	}
}

(二)缩点

1.思路:(1)先求解强连通分量

(2)每个强连通分量缩为一个点

2.注意:是否可以缩点要视题目而定

3.代码如下:

cpp 复制代码
#include <bits/stdc++.h>
#define int long long
using namespace std;

bool in_stack[10010], vis[10010];
int n, m, u, v, dfn[10010], low[10010], scc[10010], timer = 0, sc = 0;
stack<int> stk;
vector<int> g[10010];
vector<int> ans[10010];

void tarjan(int u) {
	dfn[u] = low[u] = ++timer;
	stk.push(u);
	in_stack[u] = 1;
	for (int v : g[u]) {
		if (!dfn[v]) {
			tarjan(v);
			low[u] = min(low[u], low[v]);
		} else if (in_stack[v]) {
			low[u] = min(low[u], dfn[v]);
		}
	}
	if (dfn[u] == low[u]) {
		sc++;
		while (1) {
			int v = stk.top();
			stk.pop();
			in_stack[v] = 0;
			scc[v] = sc;
			if (v == u)
				break;
		}
	}
}

signed main() {
	scanf("%lld%lld", &n, &m);
	for (int i = 1; i <= m; i++) {
		scanf("%lld%lld", &u, &v);
		g[u].push_back(v);
	}
	for (int i = 1; i <= n; i++) {
		if (!dfn[i])
			tarjan(i);
	}
	for (int i = 1; i <= n; i++) {
		ans[scc[i]].push_back(i);
	}
	printf("%lld\n", sc);
	for (int i = 1; i <= n; i++) {
		int t = scc[i];
		if (vis[t])
			continue;
		vis[t] = 1;
		for (int j = 0; j < ans[t].size(); j++)
			printf("%lld ", ans[t][j]);
		printf("\n");
	}
}

三、双连通分量相关

(一)点双连通分量相关

[一]割点

1.定义:删去以后两边不再连通的点。

2.思路:(1)对于非根 ,如果low[v] >= dfn[u] ,则**(u, v)是割点** ;

(2)对于 ,如果其孩子数超过1 ,则根和孩子们都是割点

3.证明:(1)必要性:非根时,如果(u, v)是割点,则对于v来说无法与u的祖先连通low[v] >= dfn[u] ;是根时,如果只有1个孩子,那么不可能是割点 ,因此超过1个

(2)充分性:非根时,low[v] >= dfn[u],则v回溯不到u的祖先 ,因而删掉(u, v)之后无法连通;是根时,孩子数超过1个 ,则删掉任何一对几棵子树均无法连通

4.代码如下:

cpp 复制代码
#include <bits/stdc++.h>
#define int long long
#define MAXN 100010
using namespace std;

bool flag[MAXN];
int n, m, x, y, tms = 0, dfn[MAXN], low[MAXN];
vector<int> g[MAXN];

void tarjan(int u, int pa) {
    dfn[u] = low[u] = ++tms;
    int child = 0;
    for (int v : g[u]) {
        if (!dfn[v]) {
            child++;
            tarjan(v, u);
            low[u] = min(low[u], low[v]);
            if (u != pa && low[v] >= dfn[u])
                flag[u] = 1;
        }
        else
            low[u] = min(low[u], dfn[v]);
    }
    if (u == pa && child > 1)
        flag[u] = 1;
}

signed main() {
    scanf("%lld%lld", &n, &m);
    for (int i = 1; i <= m; i++) {
        scanf("%lld%lld", &x, &y);
        g[x].push_back(y);
        g[y].push_back(x);
    }
    for (int i = 1; i <= n; i++) {
        if (!dfn[i]) 
            tarjan(i, i);
    }
    vector<int> ans;
    for (int i = 1; i <= n; i++) 
        if (flag[i])
            ans.push_back(i);
    printf("%lld\n", (int)ans.size());
    for (int i = 0; i < ans.size(); i++)
        printf("%lld%c", ans[i], " \n"[i == ans.size() - 1]);
    return 0;
}

[二]点双连通分量

1.定义:删去任何一个点 仍能连通无向图分量

2.思路:每次找到割点,割点之前栈里的点构成点双连通分量

3.证明:(1)首先可知,在DFS生成树 上BCC一定是棵

(2)然后证法几乎和上面SCC一样

4.代码如下:

cpp 复制代码
#include <bits/stdc++.h>
#define int long long
#define MAXN 2000010
using namespace std;

bool flag[MAXN];
int n, m, x, y, tms = 0, ind = 0, dfn[MAXN], low[MAXN];
vector<int> g[MAXN], bcc[MAXN];
stack<int> stk;

void tarjan(int u, int pa) {
    dfn[u] = low[u] = ++tms;
    stk.push(u);
    if (!g[u].size()) {
        bcc[++ind].push_back(u);
        return;
    }
    for (int v : g[u]) {
        if (!dfn[v]) {
            tarjan(v, u);
            low[u] = min(low[u], low[v]);
        }
        else
            low[u] = min(low[u], dfn[v]);
    }
    if (u != pa && low[u] >= dfn[pa]) {
        ind++;
        while (1) {
            int x = stk.top();
            stk.pop();
            bcc[ind].push_back(x);   
            if (u == x)
                break;
        }
        bcc[ind].push_back(pa);
    }
}

signed main() {
    scanf("%lld%lld", &n, &m);
    for (int i = 1; i <= m; i++) {
        scanf("%lld%lld", &x, &y);
        if (x == y)
            continue;
        g[x].push_back(y);
        g[y].push_back(x);
    }
    for (int i = 1; i <= n; i++) {
        if (!dfn[i]) 
            tarjan(i, i);
    }
    printf("%lld\n", ind);
    for (int i = 1; i <= ind; i++) {
        printf("%lld ", bcc[i].size());
        for (int j = 0; j < bcc[i].size(); j++) {
            printf("%lld ", bcc[i][j]);
        }
        printf("\n");
    }
    return 0;
}

(二)边双连通分量相关

[一]割边/桥

1.定义:删去以后两边不再连通的边。

2.思路:如果low[v] > dfn[u] ,则**(u, v)是桥**。

3.证明:(1)必要性:(u, v)是桥,则对于v来说无法与u所在的连通块连通low[v] > dfn[u]

(2)充分性:low[v] > dfn[u],则v能回溯到的最早的点不可能与u有直系关系/只能是它自己,即回溯不到u,因而删掉之后无法连通。

4.代码如下:

cpp 复制代码
#include <bits/stdc++.h>
#define int long long
#define MAXN 2000010
using namespace std;

bool flag[MAXN];
int n, m, x, y, tms = 0, dfn[MAXN], low[MAXN];
vector<pair<int, int>> g[MAXN];

void tarjan(int u, int idx) {
    dfn[u] = low[u] = ++tms;
    for (auto p : g[u]) {
        int v = p.first, id = p.second;
        if (!dfn[v]) {
            tarjan(v, id);
            low[u] = min(low[u], low[v]);
            if (low[v] > dfn[u])
                flag[id] = 1;
        }
        else if (idx != id)
            low[u] = min(low[u], dfn[v]);
    }
}

signed main() {
    scanf("%lld%lld", &n, &m);
    for (int i = 1; i <= m; i++) {
        scanf("%lld%lld", &x, &y);
        g[x].push_back({y, i});
        g[y].push_back({x, i});
    }
    for (int i = 1; i <= n; i++) {
        if (!dfn[i]) 
            tarjan(i, 0);
    }
    vector<int> ans;
    for (int i = 1; i <= m; i++) 
        if (flag[i])
            ans.push_back(i);
    printf("%lld\n", (int)ans.size());
    for (int i = 0; i < ans.size(); i++)
        printf("%lld ", ans[i]);
}

[二]边双连通分量

1.定义:删去任何一条边 仍能连通无向图分量

2.结论:从任意点DFS,不走桥 能到达的所有点都是该EBCC

3.证明:(1)必要性:既然是EBCC ,那么删掉任何一条边都仍然连通 ,即没有

(2)充分性:不存在桥 ,一定是边双连通子图 ,假设不是分量那么一定存在某些非桥对岸的点未到达 过,而这些点既然不存在桥一定会遍历 与假设矛盾,所以是EBCC

4.代码如下:

cpp 复制代码
#include <bits/stdc++.h>
#define int long long
#define MAXN 2000010
using namespace std;

bool flag[MAXN];
int n, m, x, y, tms = 0, ind = 0, dfn[MAXN], low[MAXN], ebcc[MAXN];
vector<pair<int, int>> g[MAXN];
vector<int> bcc[MAXN];

void tarjan(int u, int idx) {
    dfn[u] = low[u] = ++tms;
    for (auto p : g[u]) {
        int v = p.first, id = p.second;
        if (!dfn[v]) {
            tarjan(v, id);
            low[u] = min(low[u], low[v]);
            if (low[v] > dfn[u])
                flag[id] = 1;
        }
        else if (idx != id)
            low[u] = min(low[u], dfn[v]);
    }
}

void dfs(int u) {
    ebcc[u] = ind;
    bcc[ind].push_back(u);
    for (auto p : g[u]) {
        int v = p.first, id = p.second;
        if (!flag[id] && !ebcc[v])
            dfs(v);
    }
}

signed main() {
    scanf("%lld%lld", &n, &m);
    for (int i = 1; i <= m; i++) {
        scanf("%lld%lld", &x, &y);
        g[x].push_back({y, i});
        g[y].push_back({x, i});
    }
    for (int i = 1; i <= n; i++) {
        if (!dfn[i]) 
            tarjan(i, 0);
    }
    for (int i = 1; i <= n; i++) {
        if (!ebcc[i]) {
            ind++;
            dfs(i);
        }
    }
    printf("%lld\n", ind);
    for (int i = 1; i <= ind; i++) {
        printf("%lld ", bcc[i].size());
        for (int j = 0; j < bcc[i].size(); j++) {
            printf("%lld ", bcc[i][j]);
        }
        printf("\n");
    }
}
相关推荐
salipopl1 小时前
C/C++ 中 volatile 关键字详解:原理、作用与实际应用
开发语言·c++
Liangwei Lin1 小时前
LeetCode 238. 除了自身以外数组的乘积
算法
啦啦啦_99991 小时前
2. ID3决策树 & C4.5决策树
算法·决策树·机器学习
AIminminHu1 小时前
(让 C++ 程序长出大脑:从“语音遥控器”到具身智能 Agent 的进化之路)------OpenGL渲染与几何内核那点事------(二-1-(15))
开发语言·c++·agent·具身智能
技术小黑2 小时前
CNN算法实战系列02 | ResNet50V2算法实战与解析
pytorch·深度学习·算法·cnn
多加点辣也没关系2 小时前
数据结构与算法|第十五章:排序算法(下)— 非比较类排序
算法·排序算法
guo_xiao_xiao_2 小时前
YOLOv11城市道路自行车目标检测数据集-552张-bicycle-1_5
算法·yolo·目标检测
君义_noip2 小时前
CSP-J 2025 入门级 第一轮(初赛) 完善程序(1)
c++·算法·信息学奥赛·csp 第一轮
WL_Aurora2 小时前
备战蓝桥杯国赛【Day 6】
python·算法·蓝桥杯