图论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)访问到之后更新dfnu和lowu当前时间戳 ,因为刚刚访问到这棵子树所以最早的就是u本身 ,节点入栈

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

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

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

(2)为什么dfnu=lowu则形成了一个强连通分量?

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

(4)为什么lowu可以更新为lowv

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

(2)必要 性:形成强连通分量说明根节点u无法回溯 ,即dfnu=lowu充分 性:遍历完之后dfnu=lowu,说明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)对于非根 ,如果lowv >= dfnu ,则**(u, v)是割点** ;

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

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

(2)充分性:非根时,lowv >= dfnu,则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.思路:如果lowv > dfnu ,则**(u, v)是桥**。

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

(2)充分性:lowv > dfnu,则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");
    }
}
相关推荐
小江的记录本15 小时前
【JVM虚拟机】垃圾回收GC:垃圾回收算法:标记-清除、标记-复制、标记-整理、分代收集(附《思维导图》+《面试高频考点清单》)
java·jvm·后端·python·算法·安全·面试
Fre丸子_16 小时前
自定义文件夹选取功能
c++
Ulyanov16 小时前
用声明式语法重新定义Python桌面UI:QML+PySide6现代开发入门(一)
开发语言·python·算法·ui·系统仿真·雷达电子对抗仿真
数据科学小丫16 小时前
特征工程处理
人工智能·算法·机器学习
z落落17 小时前
C#参数区别
java·算法·c#
思麟呀17 小时前
C++工业级日志项目(六)异步日志器
linux·c++·windows
c2385617 小时前
vector(下)
数据结构·算法
z落落17 小时前
C# 冒泡排序+选择排序 + Array.Sort 自定义排序
数据结构·算法
wyy1851007372818 小时前
双路并行:一套匹配算法如何解决中文制单的两大核心难题
算法·ai·crm·crm系统
s_w.h18 小时前
【 linux 】文件系统
linux·运维·服务器·算法·bash