图的连通性

前言

存个模板,以免忘记了。

无向图

定义

Tarjan 算法主要用到两个值,分别为时间戳 \(dfn_x\) 以及追溯值 \(low_x\),其中 \(low_x\) 的定义为点 \(x\) 的子树中的一点能通过一条返祖边所能到的点的最小时间戳。

这里给张图:

图

9 12
1 2
2 3
3 4
4 5
5 6
3 7
7 8
7 9
1 3
3 5
3 8
4 6

根据定义,为了计算 \(low_x\),应该先令 \(low_x = dfn_x\),然后考虑从 \(x\) 出发的每条边 \((x,y)\)

若在搜索树上 \(x\)\(y\) 的父节点,则令 \(low_x=\min(low_x,low_y)\)

若无向边 \((x,y)\) 不是搜索树上的边,则令 \(low_x=\min(low_x,dfn_y)\)

下面的表格中标注了上图中每个节点的 \(low\)

\(i\) \(1\) \(2\) \(3\) \(4\) \(5\) \(6\) \(7\) \(8\) \(9\)
\(low_i\) \(1\) \(1\) \(1\) \(3\) \(3\) \(4\) \(3\) \(3\) \(7\)

code:

#include <bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define mk make_pair
#define ll long long
#define space putchar(' ')
#define enter putchar('\n')
using namespace std;

inline int read() {
	int x = 0, f = 1;
	char c = getchar();
	while (c < '0' || c > '9') f = c == '-' ? -1 : f, c = getchar();
	while (c >= '0' && c <= '9') x = (x<<3)+(x<<1)+(c^48), c = getchar();
	return x*f;
}

inline void write(int x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x/10);
	putchar('0'+x%10);
}

const int N = 1e5+5;
int n, m, idx, dfn[N], low[N];
vector <int> E[N];

inline void tarjan(int x) {
	dfn[x] = low[x] = ++idx;
	for (int y:E[x]) {
		if (!dfn[y]) {
			tarjan(y);
			low[x] = min(low[x], low[y]);
		}
		else low[x] = min(low[x], dfn[y]);
	}
}

int main() {
	n = read(), m = read();
	while (m--) {
		int x = read(), y = read();
		E[x].pb(y), E[y].pb(x);
	}
	tarjan(1);
	for (int i = 1; i <= n; ++i) write(low[i]), space;
	return 0;
}

割边

割边判定法则:无向边 \((x,y)\) 是桥,当且仅当搜索树上存在 \(x\) 的一个子节点 \(y\),满足:\(dfn_x<low_y\)

例如上图中,\((7,9)\) 为桥。

code:

#include <bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define mk make_pair
#define ll long long
#define space putchar(' ')
#define enter putchar('\n')
using namespace std;

inline int read() {
	int x = 0, f = 1;
	char c = getchar();
	while (c < '0' || c > '9') f = c == '-' ? -1 : f, c = getchar();
	while (c >= '0' && c <= '9') x = (x<<3)+(x<<1)+(c^48), c = getchar();
	return x*f;
}

inline void write(int x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x/10);
	putchar('0'+x%10);
}

const int N = 1e5+5, M = 2e5+5;
struct edge { int to, next; } e[M];
int n, m, idx, cnt, tot = 1, dfn[N], low[N], head[N], bridge[M];

inline void add(int x, int y) { e[++tot] = {y, head[x]}, head[x] = tot; }

inline void tarjan(int x, int in_edge) {
	dfn[x] = low[x] = ++idx;
	for (int i = head[x]; i; i = e[i].next) {
		int y = e[i].to;
		if (!dfn[y]) {
			tarjan(y, i);
			low[x] = min(low[x], low[y]);
			if (low[y] > dfn[x]) bridge[i] = bridge[i^1] = 1;
		}
		else if (i != (in_edge^1)) low[x] = min(low[x], dfn[y]);
	}
}

int main() {
	n = read(), m = read();
	while (m--) {
		int x = read(), y = read();
		add(x, y), add(y, x);
	}
	for (int i = 1; i <= n; ++i) if (!dfn[i]) tarjan(i, 0);
	for (int i = 2; i < tot; i += 2) if (bridge[i]) write(e[i^1].to), space, write(e[i].to), enter;
	return 0;
}

割点

割点判定法则:若 \(x\) 不是搜索树的根节点(深度优先遍历的起点),则 \(x\) 是割点当且仅当搜索树上存在 \(x\) 的一个子节点 \(y\),满足:\(dfn_x\le low_y\)。特别地,若 \(x\) 是搜索树的根节点,则 \(x\) 是割点当且仅当搜索树上存在至少两个子节点 \(y1,y2\) 满足上述条件。

例如上图中,\(3,7\) 为割点。

code:

#include <bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define mk make_pair
#define ll long long
#define space putchar(' ')
#define enter putchar('\n')
using namespace std;

inline int read() {
	int x = 0, f = 1;
	char c = getchar();
	while (c < '0' || c > '9') f = c == '-' ? -1 : f, c = getchar();
	while (c >= '0' && c <= '9') x = (x<<3)+(x<<1)+(c^48), c = getchar();
	return x*f;
}

inline void write(int x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x/10);
	putchar('0'+x%10);
}

const int N = 1e5+5, M = 2e5+5;
struct edge { int to, next; } e[M];
int n, m, tot, idx, cnt, root, dfn[N], low[N], head[N];
bool cut[N];

inline void add(int x, int y) { e[++tot] = {y, head[x]}, head[x] = tot; }

inline void tarjan(int x) {
	int flag = 0; dfn[x] = low[x] = ++idx;
	for (int i = head[x]; i; i = e[i].next) {
		int y = e[i].to;
		if (!dfn[y]) {
			tarjan(y);
			low[x] = min(low[x], low[y]);
			if (low[y] >= dfn[x]) {
				++flag;
				if (x != root || flag > 1) cut[x] = 1;
			}
		}
		else low[x] = min(low[x], dfn[y]);
	}
}

int main() {
	n = read(), m = read();
	while (m--) {
		int x = read(), y = read();
		add(x, y), add(y, x);
	}
	for (int i = 1; i <= n; ++i) if (!dfn[i]) root = i, tarjan(i);
	for (int i = 1; i <= n; ++i) if (cut[i]) ++cnt;
	write(cnt), enter;
	for (int i = 1; i <= n; ++i) if (cut[i]) write(i), space;
	return 0;
}

边双联通分量(e-DCC)

边双连通分量的计算非常容易。只需求出无向图中所有的桥,把桥都删除后,无向图会分成若干个连通块,每一个连通块就是一个边双连通分量。

例如上图中,有 \(1\) 条割边 \((7,9)\)\(2\) 个 e-DCC,分别为 \(\{1,2,3,4,5,6,7,8\},\{9\}\)

code:

#include <bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define mk make_pair
#define ll long long
#define space putchar(' ')
#define enter putchar('\n')
using namespace std;

inline int read() {
	int x = 0, f = 1;
	char c = getchar();
	while (c < '0' || c > '9') f = c == '-' ? -1 : f, c = getchar();
	while (c >= '0' && c <= '9') x = (x<<3)+(x<<1)+(c^48), c = getchar();
	return x*f;
}

inline void write(int x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x/10);
	putchar('0'+x%10);
}

const int N = 1e5+5, M = 2e5+5;
struct edge { int to, next; } e[M];
int n, m, idx, cnt, tot = 1, c[N], dfn[N], low[N], head[N], bridge[M];
vector <int> dcc[N];

inline void add(int x, int y) { e[++tot] = {y, head[x]}, head[x] = tot; }

inline void tarjan(int x, int in_edge) {
	dfn[x] = low[x] = ++idx;
	for (int i = head[x]; i; i = e[i].next) {
		int y = e[i].to;
		if (!dfn[y]) {
			tarjan(y, i);
			low[x] = min(low[x], low[y]);
			if (low[y] > dfn[x]) bridge[i] = bridge[i^1] = 1;
		}
		else if (i != (in_edge^1)) low[x] = min(low[x], dfn[y]);
	}
}

inline void dfs(int x) {
	c[x] = cnt;
	dcc[cnt].pb(x);
	for (int i = head[x]; i; i = e[i].next) {
		int y = e[i].to;
		if (!c[y] && !bridge[i]) dfs(y);
	}
}

int main() {
	n = read(), m = read();
	while (m--) {
		int x = read(), y = read();
		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]) ++cnt, dfs(i);
	write(cnt), enter;
	for (int i = 1; i <= cnt; ++i) {
		write(dcc[i].size()), space;
		for (int x:dcc[i]) write(x), space;
		enter;
	}
	return 0;
}

e-DCC 的缩点

把每个 e-DCC 看作一个节点,把桥边 (x,y)看作连接编号为 \(c_x\)\(c_y\) 的 e-DCC 对应节点的无向边,会产生一棵树(若原来的无向图不连通,则产生森林)。这种把e-DCC 收缩为一个节点的方法就称为缩点。

code:

#include <bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define mk make_pair
#define ll long long
#define space putchar(' ')
#define enter putchar('\n')
using namespace std;

inline int read() {
	int x = 0, f = 1;
	char c = getchar();
	while (c < '0' || c > '9') f = c == '-' ? -1 : f, c = getchar();
	while (c >= '0' && c <= '9') x = (x<<3)+(x<<1)+(c^48), c = getchar();
	return x*f;
}

inline void write(int x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x/10);
	putchar('0'+x%10);
}

const int N = 1e5+5, M = 2e5+5;
struct edge { int to, next; } e[M];
int n, m, idx, cnt, tot = 1, c[N], dfn[N], low[N], head[N], bridge[M];
vector <int> dcc[N], G[N];

inline void add(int x, int y) { e[++tot] = {y, head[x]}, head[x] = tot; }

inline void tarjan(int x, int in_edge) {
	dfn[x] = low[x] = ++idx;
	for (int i = head[x]; i; i = e[i].next) {
		int y = e[i].to;
		if (!dfn[y]) {
			tarjan(y, i);
			low[x] = min(low[x], low[y]);
			if (low[y] > dfn[x]) bridge[i] = bridge[i^1] = 1;
		}
		else if (i != (in_edge^1)) low[x] = min(low[x], dfn[y]);
	}
}

inline void dfs(int x) {
	c[x] = cnt;
	dcc[cnt].pb(x);
	for (int i = head[x]; i; i = e[i].next) {
		int y = e[i].to;
		if (!c[y] && !bridge[i]) dfs(y);
	}
}

int main() {
	n = read(), m = read();
	while (m--) {
		int x = read(), y = read();
		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]) ++cnt, dfs(i);
	for (int i = 2; i <= tot; ++i) {
		int x = e[i^1].to, y = e[i].to;
		if (c[x] == c[y]) continue;
		G[c[x]].pb(c[y]), G[c[y]].pb(c[x]);//G即为缩点后的树(或森林),点数cnt个 
	}
	return 0;
}

点双联通分量(v-DCC)

若某个节点为孤立点,则它自己单独构成一个 v-DCC。除了孤立点之外,点双连通分量的大小至少为 \(2\)。根据 v-DCC 定义中的“极大”性,虽然桥不属于任何 e-DCC,但是割点可能属于多个 v-DCC。

为了求出点双连通分量,需要在 Tarjan 算法的过程中维护一个栈,并按照如下方法维护栈中的元素:

  1. 当一个节点第一次被访问时,把该节点入栈

  2. 当割点判定法则中的条件 \(dfn_x\le low_y\) 成立时,无论 \(x\) 是否为根,都要:

(1) 从栈顶不断弹出节点,直至节点 \(y\) 被弹出。

(2) 刚才弹出的所有节点与节点x起构成一个 v-DCC。

例如上图中,v-DCC 有 \(4\) 个,分别为 \(\{1,2,3\},\{3,4,5,6\},\{3,7,8\},\{7,9\}\)

code:

#include <bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define mk make_pair
#define ll long long
#define space putchar(' ')
#define enter putchar('\n')
using namespace std;

inline int read() {
	int x = 0, f = 1;
	char c = getchar();
	while (c < '0' || c > '9') f = c == '-' ? -1 : f, c = getchar();
	while (c >= '0' && c <= '9') x = (x<<3)+(x<<1)+(c^48), c = getchar();
	return x*f;
}

inline void write(int x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x/10);
	putchar('0'+x%10);
}

const int N = 5e5+5, M = 4e6+5;
struct edge { int to, next; } e[M];
int n, m, idx, cnt, top, tot, root, dfn[N], low[N], stk[N], cut[N], head[N];
vector <int> dcc[N];

inline void add(int x, int y) { e[++tot] = {y, head[x]}, head[x] = tot; }

inline void tarjan(int x) {
	int flag = 0; dfn[x] = low[x] = ++idx; stk[++top] = x;
	if (x == root && !head[x]) return (void)(dcc[++cnt].pb(x));
	for (int i = head[x]; i; i = e[i].next) {
		int y = e[i].to;
		if (!dfn[y]) {
			tarjan(y);
			low[x] = min(low[x], low[y]);
			if (low[y] >= dfn[x]) {
				++flag, ++cnt;
				if (x != root || flag > 1) cut[x] = 1;
				while (stk[top] != y) dcc[cnt].pb(stk[top--]);
				--top, dcc[cnt].pb(x), dcc[cnt].pb(y);
			}
		}
		else low[x] = min(low[x], dfn[y]);
	}
}

int main() {
	n = read(), m = read();
	while (m--) {
		int x = read(), y = read();
		if (x == y) continue;
		add(x, y), add(y, x);
	}
	for (int i = 1; i <= n; ++i) if (!dfn[i]) root = i, tarjan(i);
	write(cnt), enter;
	for (int i = 1; i <= cnt; ++i) {
		write(dcc[i].size()), space;
		for (int x:dcc[i]) write(x), space;
		enter;
	}
	return 0;
}

v-DCC 的缩点

v-DCC 的缩点比 e-DCC 要复杂一些,因为一个割点可能属于多个 v-DCC。设图中共有 \(p\) 个割点和 \(t\) 个 v-DCC。我们建立一张包含 \(p+t\) 个节点的新图,把每个 v-DCC 和每个割点都作为新图中的节点,并在每个割点与包含它的所有 v-DCC 之间连边。

code:

#include <bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define mk make_pair
#define ll long long
#define space putchar(' ')
#define enter putchar('\n')
using namespace std;

inline int read() {
	int x = 0, f = 1;
	char c = getchar();
	while (c < '0' || c > '9') f = c == '-' ? -1 : f, c = getchar();
	while (c >= '0' && c <= '9') x = (x<<3)+(x<<1)+(c^48), c = getchar();
	return x*f;
}

inline void write(int x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x/10);
	putchar('0'+x%10);
}

const int N = 1e5+5, M = 2e5+5;
struct edge { int to, next; } e[M];
int n, m, idx, cnt, top, tot = 1, root, c[N], id[N], dfn[N], low[N], cut[N], stk[N], head[N];
vector <int> dcc[N], G[N];

inline void add(int x, int y) { e[++tot] = {y, head[x]}, head[x] = tot; }

inline void tarjan(int x) {
	int flag = 0; dfn[x] = low[x] = ++idx; stk[++top] = x;
	if (x == root && !head[x]) return (void)(dcc[++cnt].pb(x));
	for (int i = head[x]; i; i = e[i].next) {
		int y = e[i].to;
		if (!dfn[y]) {
			tarjan(y);
			low[x] = min(low[x], low[y]);
			if (low[y] >= dfn[x]) {
				++flag, ++cnt;
				if (x != root || flag > 1) cut[x] = 1;
				while (stk[top] != y) dcc[cnt].pb(stk[top--]);
				--top, dcc[cnt].pb(x), dcc[cnt].pb(y);
			}
		}
		else low[x] = min(low[x], dfn[y]);
	}
}

int main() {
	n = read(), m = read();
	while (m--) {
		int x = read(), y = read();
		if (x == y) continue;
		add(x, y), add(y, x);
	}
	for (int i = 1; i <= n; ++i) if (!dfn[i]) root = i, tarjan(i);
	idx = cnt;
	for (int i = 1; i <= n; ++i) if (cut[i]) id[i] = ++idx;
	for (int i = 1; i <= cnt; ++i)
		for (int x:dcc[i]) {
			if (cut[x]) G[i].pb(id[x]), G[id[x]].pb(i);//G即为缩点后的树(或森林),点数idx个 
			else c[x] = i;
		}
	return 0;
}

有向图

定义

这里还是放张图(其中粗边为树边):

图

9 13
1 2
2 3
3 4
4 5
1 6
6 8
6 7
8 9
1 5
5 2
7 4
8 7
9 6

给定有向图 \(G=(V,E)\),若存在 \(r\in V\),满足从 \(r\) 出发能够到达 \(V\) 中所有的点,则称 \(G\) 是一个“流图”(Flow Graph),记为 \((G,r)\),其中 \(r\) 称为流图的源点。

流图中的每条有向边 \((x,y)\) 必然是以下四种之一:

  1. 树枝边,指搜索树中的边,即 \(x\)\(y\) 的父节点。

  2. 前向边,指搜索树中 \(x\)\(y\) 的祖先节点。

  3. 后向边,指搜索树中 \(y\)\(x\) 的祖先节点。

  4. 横叉边,指除了以上三种情况之外的边,它一定满足 \(dfn_y<dfn_x\)

强连通分量(SCC)

一个环一定是强连通图。如果既存在从 \(x\)\(y\) 的路,也存在从 \(y\)\(x\) 的路径,那么 \(x,y\) 显然在一个环中。因此,Tarjan 算法的基本思路就是对于每个点,尽量找到与它一起能构成环的所有节点。

容易发现,“前向边” \((x,y)\) 没有什么用处,因为搜索树上本来就存在从 \(x\)\(y\) 的路径。“后向边” \((x,y)\) 非常有用,因为它可以和搜索树上从 \(y\)\(x\) 的路径一起构成环。“横叉边”\((x,y)\)视情况而定,如果从 \(y\) 出发能找到一条路径回到 \(x\) 的祖先节点,那么 \((x,y)\) 就是有用的。

为了找到通过“后向边”和“横叉边”构成的环,Tarjan 算法在深度优先遍历的同时维护了一个栈。当访问到节点 \(x\) 时,栈中需要保存以下两类节点:

  1. 搜索树上 \(x\) 的祖先节点,记为集合 \(anc(x)\)

\(y\in anc(x)\)。若存在后向边 \((x,y)\),则 \((x,y)\)\(y\)\(x\) 的路径一起形成环。

  1. 已经访问过,并且存在一条路径到达 \(anc(x)\) 的节点。

\(z\) 是一个这样的点,从 \(z\) 出发存在一条路径到达 \(y\in anc(x)\)。若存在横叉边 \((x,z)\),则 \((x,z)\)\(z\)\(y\) 的路径、\(y\)\(x\) 的路径形成一个环。

综上所述,栈中的节点就是能与从 \(x\) 出发的“后向边”和“横叉边”形成环的节点。进而可以引入“追溯值”的概念。

追溯值

\(subtree(x)\) 表示流图的搜索树中以 \(x\) 为根的子树。\(x\) 的追溯值 \(low_x\) 定义为满足以下条件的节点的最小时间戳:

  1. 该点在栈中。

  2. 存在一条从 \(subtree(x)\) 出发的有向边,以该点为终点。

根据定义,Tarjan 算法按照以下步骤计算“追溯值”:

  1. 当节点 \(x\) 第一次被访问时,把 \(x\) 入栈,初始化 \(low_x=dfn_x\)

  2. 扫描从 x出发的每条边 \((x,y)\)

(1) 若 \(y\) 没被访问过,则说明 \((x,y)\) 是树枝边,递归访问 \(y\),从 \(y\) 回溯之后,令 \(low_x\) = \min(low_x,low_y)$

(2) 若 \(y\) 被访问过并且 \(y\) 在栈中,则令 \(low_x=\min(low_x,dfn_y)\)

  1. \(x\) 回溯之前,判断是否有 \(low_x=dfn_x\)。若成立,则不断从栈中弹出节点,直至 \(x\) 出栈。

下面表格中为上图各点的 \(low_x\) 值。

\(i\) \(1\) \(2\) \(3\) \(4\) \(5\) \(6\) \(7\) \(8\) \(9\)
\(low_i\) \(1\) \(2\) \(2\) \(2\) \(2\) \(6\) \(7\) \(6\) \(6\)

code:

#include <bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define mk make_pair
#define ll long long
#define space putchar(' ')
#define enter putchar('\n')
using namespace std;

inline int read() {
	int x = 0, f = 1;
	char c = getchar();
	while (c < '0' || c > '9') f = c == '-' ? -1 : f, c = getchar();
	while (c >= '0' && c <= '9') x = (x<<3)+(x<<1)+(c^48), c = getchar();
	return x*f;
}

inline void write(int x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x/10);
	putchar('0'+x%10);
}

const int N = 1e5+5, M = 2e5+5;
struct edge { int to, next; } e[M];
int n, m, tot, idx, top, dfn[N], low[N], stk[N], ins[N], head[N];

inline void add(int x, int y) { e[++tot] = {y, head[x]}, head[x] = tot; }

inline void tarjan(int x) {
	dfn[x] = low[x] = ++idx, stk[++top] = x, ins[x] = 1;
	for (int i = head[x]; i; i = e[i].next) {
		int y = e[i].to;
		if (!dfn[y]) {
			tarjan(y);
			low[x] = min(low[x], low[y]);
		}
		else if (ins[y]) low[x] = min(low[x], dfn[y]);
	}
	if (dfn[x] == low[x]) {
		while (stk[top] != x) ins[stk[top--]] = 0;
		ins[stk[top--]] = 0;
	}
}

int main() {
	n = read(), m = read();
	while (m--) {
		int x = read(), y = read();
		add(x, y);
	}
	for (int i = 1; i <= n; ++i) if (!dfn[i]) tarjan(i);
	for (int i = 1; i <= n; ++i) write(low[i]), space;
	return 0;
}

强连通分量判定法则

在追溯值的计算过程中,若从 \(x\) 回溯前,有 \(low_x=dfn_x\) 成立,则中从 \(x\) 到栈顶的所有节点构成一个强连通分量。

例如上图,SCC 有 \(4\) 个分别为 \(\{1\},\{7\},\{2,3,4,5\},\{6,8,9\}\)

code:

#include <bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define mk make_pair
#define ll long long
#define space putchar(' ')
#define enter putchar('\n')
using namespace std;

inline int read() {
	int x = 0, f = 1;
	char c = getchar();
	while (c < '0' || c > '9') f = c == '-' ? -1 : f, c = getchar();
	while (c >= '0' && c <= '9') x = (x<<3)+(x<<1)+(c^48), c = getchar();
	return x*f;
}

inline void write(int x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x/10);
	putchar('0'+x%10);
}

const int N = 1e5+5, M = 2e5+5;
struct edge { int to, next; } e[M];
int n, m, tot, cnt, idx, top, c[N], dfn[N], low[N], stk[N], ins[N], head[N];
vector <int> scc[N];

inline void add(int x, int y) { e[++tot] = {y, head[x]}, head[x] = tot; }

inline void tarjan(int x) {
	dfn[x] = low[x] = ++idx, stk[++top] = x, ins[x] = 1;
	for (int i = head[x]; i; i = e[i].next) {
		int y = e[i].to;
		if (!dfn[y]) {
			tarjan(y);
			low[x] = min(low[x], low[y]);
		}
		else if (ins[y]) low[x] = min(low[x], dfn[y]);
	}
	if (dfn[x] == low[x]) {
		++cnt;
		while (stk[top] != x) c[stk[top]] = cnt, scc[cnt].pb(stk[top]), ins[stk[top--]] = 0;
		--top, c[x] = cnt, scc[cnt].pb(x), ins[x] = 0;
	}
}

int main() {
	n = read(), m = read();
	while (m--) {
		int x = read(), y = read();
		add(x, y);
	}
	for (int i = 1; i <= n; ++i) if (!dfn[i]) tarjan(i);
	write(cnt), enter;
	for (int i = 1, l; i <= cnt; ++i) {
		write(scc[i].size()), space;
		for (int x:scc[i]) write(x), space;
		enter;
	}
	return 0;
}

SCC 的缩点

与无向图 e-DCC 的缩点类似,我们也可以把每个 SCC 缩成一个点。对原图中的每条有向边 \((x,y)\),若 \(c[x]\ne c[y]\),则在编号为 \(c[x]\) 与编号为 \(c[y]\) 的 SCC 之间连边。最后,我们会得到一张有向无环图。

code:

#include <bits/stdc++.h>
#define fi first
#define se second
#define pb push_back
#define mk make_pair
#define ll long long
#define space putchar(' ')
#define enter putchar('\n')
using namespace std;

inline int read() {
	int x = 0, f = 1;
	char c = getchar();
	while (c < '0' || c > '9') f = c == '-' ? -1 : f, c = getchar();
	while (c >= '0' && c <= '9') x = (x<<3)+(x<<1)+(c^48), c = getchar();
	return x*f;
}

inline void write(int x) {
	if (x < 0) x = -x, putchar('-');
	if (x > 9) write(x/10);
	putchar('0'+x%10);
}

const int N = 1e5+5, M = 2e5+5;
struct edge { int to, next; } e[M];
int n, m, tot, cnt, idx, top, c[N], dfn[N], low[N], stk[N], ins[N], head[N];
vector <int> scc[N], G[N];

inline void add(int x, int y) { e[++tot] = {y, head[x]}, head[x] = tot; }

inline void tarjan(int x) {
	dfn[x] = low[x] = ++idx, stk[++top] = x, ins[x] = 1;
	for (int i = head[x]; i; i = e[i].next)
		if (!dfn[e[i].to]) {
			tarjan(e[i].to);
			low[x] = min(low[x], low[e[i].to]);
		}
		else if (ins[e[i].to]) low[x] = min(low[x], dfn[e[i].to]);
	if (dfn[x] == low[x]) {
		++cnt;
		while (stk[top] != x) c[stk[top]] = cnt, scc[cnt].pb(stk[top]), ins[stk[top--]] = 0;
		--top, c[x] = cnt, scc[cnt].pb(x), ins[x] = 0;
	}
}

int main() {
	n = read(), m = read();
	while (m--) {
		int x = read(), y = read();
		add(x, y);
	}
	for (int i = 1; i <= n; ++i) if (!dfn[i]) tarjan(i);
	for (int x = 1; x <= n; ++x)
		for (int i = head[x]; i; i = e[i].next) {
			int y = e[i].to;
			if (c[x] != c[y]) G[c[x]].pb(c[y]), G[c[y]].pb(c[x]);//G即为缩点后的树(或森林),点数cnt个 
		}
	return 0;
}
posted @ 2023-12-30 11:01  123wwm  阅读(26)  评论(0)    收藏  举报