边双连通分量

\(\text{luogu-8436}\)

对于一个 \(n\) 个节点 \(m\) 条无向边的图,求其边双连通分量的个数,并且输出每个边双连通分量。

\(1 \le n \le 5 \times 10^5\)\(1 \le m \le 2 \times 10^6\)


以下题解部分来自于 强连通分量 | 点双连通分量 | 边双连通分量 - 知乎

在一张连通的无向图中,对于任意的两个顶点 \(u\)\(v\) ,如果任意去掉一条边后,\(u\)\(v\) 之间的连通性仍然没有发生改变,那么 \(u\)\(v\) 就是边双连通的。对于一张无向图,其边双连通的极大子图,就称为边双连通分量

对于求解具体的边双连通分量,我们可以先求出无向图的割边,之后再一次遍历这张图,跳过割边,这样我们就可以求得具体的边双连通分量了。

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
#define MAXN 500005
#define MAXM 2000005
#define pii pair<long long, long long>
#define fi first
#define se second

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, m, dfn[MAXN], low[MAXN], dn;
vector<vector<long long> > scc;
bool f[MAXM], vis[MAXN];
vector<pii > v[MAXN];

void tarjan(long long x, long long id) {
	dfn[x] = low[x] = ++ dn;
	for(auto it : v[x]) {
		long long y = it.fi, d = it.se;
		if(!dfn[y]) tarjan(y, d), low[x] = min(low[x], low[y]);
		else if(d != id) low[x] = min(low[x], dfn[y]);
	}
	if(dfn[x] == low[x] && id != -1) f[id] = 1;
	return;
}

void dfs(long long x) {
	vis[x] = 1;
	scc.back().push_back(x);
	for(auto it : v[x]) if(!f[it.se] && !vis[it.fi]) dfs(it.fi);
	return;
}

int main() {
	n = read(), m = read();
	for(int i = 1; i <= m; i ++) {
		long long x = read(), y = read();
		v[x].push_back({y, i}), v[y].push_back({x, i});
	}
	for(int i = 1; i <= n; i ++) if(!dfn[i]) tarjan(i, -1);
	for(int i = 1; i <= n; i ++) if(!vis[i]) scc.push_back({}), dfs(i);
	cout << scc.size() << "\n";
	for(auto it : scc) {
		cout << it.size() << " ";
		for(auto x : it) cout << x << " ";
		cout << "\n";
	}
	return 0;
}

\(\text{luogu-2860}\)

为了从 \(F\) 个牧场(编号为 \(1\)\(F\))中的一个到达另一个牧场,贝西和其他牛群被迫经过腐烂苹果树附近。奶牛们厌倦了经常被迫走特定的路径,想要修建一些新路径,以便在任意一对牧场之间总是有至少两条独立的路线可供选择。目前在每对牧场之间至少有一条路径,他们希望至少有两条。当然,他们只能在官方路径上从一个牧场移动到另一个牧场。

给定当前 \(R\) 条路径的描述,每条路径恰好连接两个不同的牧场,确定必须修建的最少新路径数量(每条新路径也恰好连接两个牧场),以便在任意一对牧场之间至少有两条独立的路线。若两条路线不使用相同的路径,即使它们沿途访问相同的中间牧场,也被视为独立的。

在同一对牧场之间可能已经有多条路径,你也可以修建一条新路径连接与某条现有路径相同的牧场。

\(1\le F\le 5000\)\(F-1\le R\le 10^4\)


首先我们发现,对于边双连通分量其实不需要考虑,缩点即可。

缩点后图变成了一颗树,我们只需要把叶子节点两两连边,就可以使得原图是一个极大边双连通分量。需要注意的是,若叶子节点为奇数个,需要连一半多一条边,实际上就是上取整。

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
#define MAXN 10005
#define pii pair<long long, long long>
#define fi first
#define se second

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, m, dfn[MAXN], low[MAXN], dn, c[MAXN], d[MAXN], ans;
vector<vector<long long> > scc;
bool f[MAXN], vis[MAXN];
vector<pii > v[MAXN];

void tarjan(long long x, long long id) {
	dfn[x] = low[x] = ++ dn;
	for(auto it : v[x]) {
		long long y = it.fi, d = it.se;
		if(!dfn[y]) tarjan(y, d), low[x] = min(low[x], low[y]);
		else if(d != id) low[x] = min(low[x], dfn[y]);
	}
	if(dfn[x] == low[x] && id != -1) f[id] = 1;
	return;
}

void dfs(long long x) {
	vis[x] = 1, c[x] = scc.size();
	scc.back().push_back(x);
	for(auto it : v[x]) if(!f[it.se] && !vis[it.fi]) dfs(it.fi);
	return;
}

int main() {
	n = read(), m = read();
	for(int i = 1; i <= m; i ++) {
		long long x = read(), y = read();
		v[x].push_back({y, i}), v[y].push_back({x, i});
	}
	for(int i = 1; i <= n; i ++) if(!dfn[i]) tarjan(i, -1);
	for(int i = 1; i <= n; i ++) if(!vis[i]) scc.push_back({}), dfs(i);
	for(int i = 1; i <= n; i ++) for(auto it : v[i])
		if(c[i] != c[it.fi]) d[c[i]] ++, d[c[it.fi]] ++;
	for(int i = 1; i <= scc.size(); i ++) if(d[i] == 2) ans ++;
	cout << (ans + 1) / 2 << "\n";
	return 0;
}

\(\text{luogu-2783}\)

给定 \(n\) 个点 \(m\) 条边无向图,把图中所有的环变为一个点,求变化后某两个点之间有多少个点。

两个点不成环,且输出答案时转为二进制。

\(1<n\le10 ^ 4\)\(1<m\le5\times 10 ^ 4\)


有点奇怪的一道题,感觉描述不太清楚。

这题的缩点实际上是无向图的类强连通分量,也就是说在无向图中,至少两个点的环才成为类强连通分量。我们需要把图中的类强连通分量,进行缩点。

只需要在 Tarjan 求强连通的过程中加个特判即可,比较神秘。

接着就是求两个点之间的距离,直接用 lca 倍增求。

注意:重构图时注意只需要连单向边,因为原图是无向图,每条边会被遍历到两次。

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
#define MAXN 10005

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, m, q, dfn[MAXN], low[MAXN], dn, s[MAXN], in[MAXN], lg[MAXN];
long long top, is[MAXN], cnt, scc[MAXN], dep[MAXN], fa[MAXN][30];
vector<long long> v[MAXN], g[MAXN];
bool bt[MAXN];

void tarjan(long long x, long long fa) {
	low[x] = dfn[x] = ++ dn, s[++ top] = x, is[x] = 1;
	for(auto y : v[x]) {
		if(y == fa) continue;
		if(!dfn[y]) tarjan(y, x), low[x] = min(low[x], low[y]);
		else if(is[y]) low[x] = min(low[x], dfn[y]);
	}
	if(dfn[x] == low[x]) {
		cnt ++; 
		do {
			scc[s[top]] = cnt, is[s[top]] = 0;
		} while(s[top --] != x);
	}
	return;
}

void dfs(long long x, long long f) {
	fa[x][0] = f, dep[x] = dep[f] + 1;
	for(int i = 1; i <= lg[dep[x]]; i ++)
		fa[x][i] = fa[fa[x][i - 1]][i - 1];
	for(auto y : g[x]) if(y != f) dfs(y, x);
	return;
}

long long lca(long long x, long long y) {
	if(dep[x] < dep[y]) swap(x, y);
	while(dep[x] > dep[y])
		x = fa[x][lg[dep[x] - dep[y]] - 1];
	if(x == y) return x;
	for(int i = lg[dep[x]] - 1; i >= 0; i --)
		if(fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];
	return fa[x][0];
}

void cg(long long x) {
	long long res = 0;
	while(x) bt[++ res] = x % 2, x /= 2;
	for(int i = res; i >= 1; i --) cout << bt[i];
	cout << "\n"; return;
}

int main() {
	n = read(), m = read();
	for(int i = 1; i <= m; i ++) {
		long long x = read(), y = read();
		v[x].push_back(y), v[y].push_back(x);
	}
	for(int i = 1; i <= n; i ++) if(!dfn[i]) tarjan(i, -1);
	for(int i = 1; i <= n; i ++) for(auto j : v[i]) 
		if(scc[i] != scc[j]) g[scc[i]].push_back(scc[j]); 
	for(int i = 1; i < MAXN; i ++) lg[i] = lg[i >> 1] + 1;
	dfs(1, 0), q = read();
	while(q --) {
		long long x = scc[read()], y = scc[read()];
		cg(dep[x] + dep[y] - 2 * dep[lca(x, y)] + 1);
	}
	return 0;
}

\(\text{luogu-7924}\)

小 A 是一个热衷于旅行的旅行家。有一天,他来到了一个城市,这个城市由 \(n\) 个景点与 \(m\) 条连接这些景点的道路组成。每个景点有一个美观度 \(a_i\)。定义一条旅游路径为两个景点之间的一条非严格简单路径,也就是点可以重复经过,而边不可以。

接下来有 \(q\) 个旅游季,每个旅游季中,小 A 将指定两个顶点 \(x\)\(y\),然后他将走遍 \(x\)\(y\)所有旅游路径。 所有旅游季结束后,小 A 会统计他所经过的所有景点的美观度之和(重复经过一个景点只统计一次美观度)。他希望你告诉他这个美观度之和。

\(3 \leq n \leq 5 \times 10^5\)\(m \leq 2 \times 10^6\)\(q\le10^6\)\(1 \leq a_i \leq 100\),且该图联通,没有重边和自环。


非常毒瘤的一道题,指的是数据。

实际上思路并不难,显然若能到达边双其中的一个点,则这个边双所有点都可达。

于是考虑把边双缩成点,这样图就变成了一颗树。路径 \(x \to y\) 也就是从 \(x\) 所在的边双到 \(y\) 所在的边双,只能走最短路径。需要特判 \(x,y\) 在同一个边双里的情况。

路径上经过的点都是可达点,由于每个点不能重复计算贡献,可以用树上差分解决。

于是这道题就做完了,但是数据卡常,不放代码了,因为我不想卡了。

\(\text{luogu-6658}\)

对于一张无向图 \(G = (V, E)\)

  • 我们称两个点 \(u, v ~ (u, v \in V, u \neq v)\) 是边三连通的,当且仅当存在三条从 \(u\) 出发到达 \(v\) 的,相互没有公共边的路径。
  • 我们称一个点集 \(U ~ (U \subseteq V)\) 是边三连通分量,当且仅当对于任意两个点 \(u', v' ~ (u', v' \in U, u' \neq v')\) 都是边三连通的。
  • 我们称一个边三连通分量 \(S\) 是极大边三连通分量,当且仅当不存在 \(u \not \in S\)\(u \in V\),使得 \(S \cup \{u\}\) 也是边三连通分量。

给出一个 \(n\) 个点,\(m\) 条边的无向图 \(G = (V, E)\)\(V = \{1, 2, \ldots, n\}\),请求出其所有的极大边三连通分量。

\(1 \le n, m \le 5 \times 10 ^ 5\)\(1 \le u, v \le n\)。可能有重边和自环。


边三连通分量模板题。

以下部分题解来自于 关于边三连通分量 - 洛谷专栏

感觉口胡了很多遍的模板算法,快 NOI 了才想起来写写代码。其实边三的代码很好写,网上许多资料都写麻烦了。

边联通性其实是一个很能扩展的东西。两个点之间如果最少要割开 \(k\) 条边才能使它们之间不联通,称这两个点的边联通度为 \(k\)。称两个点之间是 \(k\) 边联通的,当且仅当这两个点的边联通度 \(\ge k\)

边联通性具有很好的传递性。即若 \(x,y\)\(k\) 边联通的,\(y,z\)\(k\) 边联通的,那么 \(x,z\)\(k\) 边联通的。证明考虑假设一个大小 \(<k\) 的边集割开了 \(x,z\),那么这个割集势必没有割开 \(x,y\)\(y,z\),此时 \(x,z\) 仍联通矛盾。\(k\) 边联通分量就是把 \(k\) 边联通的点连边形成的连通块。由上面的传递性我们可以知道边联通分量中的点两两 \(k\) 边联通。

这件事告诉我们了 \(k\) 边联通分量总是可以被良好定义且结构优美的,但是点联通分量光是在 \(k=2\) 的时候就略显丑陋——有可能一个点在多个点双中。

如何判断 \(k\) 边联通性呢?我们考虑一个比较通用的问题:给定一张图的 \(k\) 条边,问其是否是边割集。

做法是考虑经典的割空间与环空间。极小的边割集定义为对于一个边割集加回其中一条边后,图的联通性有变化,或者说你考虑把图仍以分成两个部分,那么跨过这两个部分的边组成一个极小的边割集。一个图的割空间是一个由所有极小的边割集组成的线性空间,即其在对称差(异或)运算下封闭。一个图的回路空间也是线性空间(回路指每个连通块都有欧拉回路的边集,也就是每个点均与其中的偶数条边相邻)。这两个空间互为正交补,即边割集与回路的公共部分一定大小为偶数,且与一个回路公共部分大小为偶数的一定是边割集。

由一些经典结论我们知道只用非树边和树边形成的简单环异或就可以得到所有的环。所以判定一个边集是不是极小的边割集可以异或哈希给非树边随机赋权,然后让树边的权值等于所有覆盖它的非树边权值的异或。这样如果一个边集权值异或和为 0,说明其与某个回路正交。

判断一个集合是否是边割集就是看存不存在一个非空子集是极小边割集,那么就是问这个集合的权值是不是线性相关。

现在对于 \(k=3\) 的情况,我们应用这个技巧给边随机赋权。那么一个点集边三联通相当于其中所有边的大小不超过三的子集线性不相关。线性相关无非这么几种情况:

  • 存在一条树边边权为 0。即这条边是割边,在三联通分量中我们需要割开这条树边。

  • 存在一个树边和非树边权值相等。我们也需要割开这条树边。

  • 存在两条树边权值相等。此时中间的部分需要跟两边的部分隔开。

发现我们要做一个分割连通块的操作,一种方法是 dfs 打标记,但这样细节还是不够少。我发现 tzc_wk 博客里的方法是又好写又好记。我们同样采用异或哈希的手法,一个连通块需要跟其它部分分开相当于这个连通块需要全体异或上一个与众不同的值。那么对于前两种情况,打一个子树异或标记,对于第三种一上一下打两个子树异或标记,就可以区分出中间的连通块。

由于是 dfs 树,非树边一定直上直下,那么所有权值相等的树边肯定排列在一条直上直下的树链上,显然我们只需要处理这条链上相邻的两条边就够了。我们只需要开个全局 Hash 表,在 dfs 回溯时找到子树中第一条跟它权值相同的边处理第三种情况。(因为跟它权值相同的边都在一条链上,所以如果 Hash 表中有那么一定在当前边的子树中)

只需要两次 dfs。如果你选择多跑一次 tarjan 处理第一种情况有点多此一举了。


以下代码是我编写的,加注释版,更好理解。

#include<iostream>
#include<cstdio>
#include<random>
#include<ctime>
#include<bits/extc++.h>
using namespace std;
using namespace __gnu_pbds;
#define MAXN 500005
#define ull unsigned long long	// 64位无符号整型,存异或哈希值无符号位干扰

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

// 全局变量定义
long long n, m;			// n:节点数 m:边数
long long dfn[MAXN], dn;	// dfn[]:节点的DFS访问时间戳 dn:时间戳计数器
long long hd[MAXN], to[MAXN << 1], nxt[MAXN << 1], tot = 1; // 邻接表存图:hd表头 to边终点 nxt下一条边
gp_hash_table<ull, long long> mp, id; // mp:树边权值->节点编号  id:异或标记->分量下标
vector<vector<long long> > res;	// 存储最终答案:每个vector存一个边三连通分量的所有节点
bool ot[MAXN << 1], rt[MAXN];	// ot[i]=1:边i是DFS树边  rt[i]=1:节点i是连通块的根节点
gp_hash_table<ull, bool> ex;	// 存非树边的随机权值,标记该权值是否存在
mt19937_64 rnd(time(0));		// 64位梅森旋转随机数生成器,生成异或哈希的随机值
ull w[MAXN], e[MAXN];			// w[]:节点对应父树边的异或权值  e[]:节点的异或分割标记(核心)

// 邻接表加边函数:无向图存双向边,tot从1开始保证i^1能找到反向边
void add(long long x, long long y) {
	nxt[++ tot] = hd[x], hd[x] = tot;
	to[tot] = y; return;
}

// 第一次DFS:核心处理
// x:当前遍历节点  lst:当前节点的父边编号,用于跳过反向边
void dfs(long long x, long long lst) {
	dfn[x] = ++ dn;	// 给当前节点打上DFS访问时间戳,标记已访问
	// 遍历当前节点的所有邻边
	for(int i = hd[x]; i; i = nxt[i]) {
		long long y = to[i]; // 取出当前边的终点
		if(i == lst) continue; // 跳过父边,避免DFS回头遍历
		// 情况1:y已经被访问过 → 这条边是非树边(回边)
		if(dfn[y]) {
			if(dfn[y] < dfn[x]) { // 只处理祖先->后代的回边,避免重复处理同一条非树边
				ull t = rnd();    // 给这条非树边分配一个随机的64位异或权值
				w[x] ^= t, w[y] ^= t; // 被该非树边覆盖的树边权值异或该随机值
				ex[t] = 1;        // 标记该随机值是非树边的权值
			} 
		}
		// 情况2:y未被访问过 → 这条边是树边
		else {
			ot[i] = 1;               // 标记该边为树边
			dfs(y, i ^ 1);           // 递归遍历子节点,i^1是y的父边编号(反向边)
			w[x] ^= w[y];            // 回溯合并权值:父节点的树边权值异或子节点的树边权值
		}
	}
	// DFS回溯阶段:判定三种需要分割的情况,打异或分割标记
	if(ex.find(w[x]) != ex.end()) {
		// 情况1:当前树边权值是0(割边) 或 等于某条非树边权值 → 异或随机值分割
		e[x] ^= rnd();
	} else {
		auto it = mp.find(w[x]);
		if(it != mp.end()) {
			// 情况2:当前树边权值和子树中某条树边权值相等 → 上下两点异或同个随机值分割中间连通块
			ull t = rnd();
			e[it->second] ^= t, e[x] ^= t;
			it->second = x; // 更新哈希表,保留子树中最近的同权值节点
		} else {
			mp[w[x]] = x; // 无同权值树边,存入哈希表
		}
	}
	return;
}

// 第二次DFS:传递异或分割标记,收集边三连通分量的节点
void dfs1(long long x) {
	// 遍历当前节点的所有邻边,只处理树边
	for(int i = hd[x]; i; i = nxt[i]) {
		if(ot[i]) { // 树边才需要传递标记,非树边无意义
			e[to[i]] ^= e[x]; // 子节点继承父节点的异或标记,核心传递操作
			dfs1(to[i]);      // 递归处理子节点
		}
	}
	// 将当前节点归类到对应的边三连通分量中
	if(id.find(e[x]) != id.end()) {
		// 已有该标记对应的分量,直接加入节点
		res[id[e[x]]].emplace_back(x);
	} else {
		// 无该标记,新建一个分量,记录下标
		id[e[x]] = res.size();
		res.emplace_back(1, x);
	}
	return;
}

int main() {
	n = read(), m = read(); // 读入节点数和边数
	// 读入m条边,无向图存双向边
	for(int i = 1; i <= m; i ++) {
		long long x = read(), y = read();
		add(x, y), add(y, x);
	}
	ex[0] = 1; // 关键初始化:标记0为非树边权值,处理树边权值为0的割边情况
	// 遍历所有未访问的节点,处理多个连通块
	for(int i = 1; i <= n; i ++) {
		if(!dfn[i]) { // 节点未被访问,作为连通块的根节点
			e[i] ^= rnd(); // 根节点赋初始随机异或标记
			dfs(i, 0);     // 跑第一次DFS,计算权值+打分割标记
			rt[i] = 1;     // 标记该节点是连通块的根节点
		}
	}
	// 对每个连通块的根节点跑第二次DFS,传递标记+收集分量
	for(int i = 1; i <= n; i ++) if(rt[i]) dfs1(i);
	// 排序:每个分量内节点升序,分量整体字典序,满足题目输出格式
	for(auto &it : res) sort(it.begin(), it.end());
	sort(res.begin(), res.end());
	// 输出答案
	cout << res.size() << "\n"; // 输出边三连通分量的数量
	for(auto it : res) {        // 输出每个分量的所有节点
		for(auto x : it) cout << x << " ";
		cout << "\n";
	}
	return 0;
}

\(\text{luogu-4214}\)

你被雇佣升级一个旧果汁加工厂的橙汁运输系统。系统有管道和节点构成。每条管道都是双向的,且每条管道的流量都是 \(1\) 升每秒。管道可能连接节点,每个节点最多可以连接 \(3\) 条管道。节点的流量是无限的。节点用整数 \(1\)\(n\) 来表示。

在升级系统之前,你需要对现有系统进行分析。对于两个不同节点 \(s\)\(t\)\(s-t\) 的流量被定义为:当 \(s\) 为源点,\(t\) 为汇点,从 \(s\) 能流向 \(t\) 的最大流量。

以下面的第一组样例数据为例,\(1-6\) 的流量为 \(3\)\(1-2\) 的流量为 \(2\)

计算每一对满足 \(a<b\) 的节点 \(a-b\) 的流量的和。

\(2 \le n \le 3000\)\(0 \le m \le 4500\)


由于最大流等于最小割,所以最大流一定 \(\le 3\)

那么可以分类讨论:

  • \(x,y\) 不连通,则最大流为 \(0\)
  • \(x,y\) 连通。
    • \(dcc_x \ne dcc_y\),即 \(x,y\) 不在一个边双连通分量中,则最大流为 \(1\)
    • \(dcc_x = dcc_y\)
      • \(x,y\) 边三联通,则最大流为 \(3\)
      • 否则为 \(2\)

判边双连通跑一边 tarjan 即可,边三连通则可以根据定义,原图中删除任意一条边后,若 \(x,y\) 仍边双连通,则 \(x,y\) 边三连通。

于是判边三连通的话可以删边之后跑 \(m\) 遍 tarjan,用哈希判断两个点是否每一轮 \(dcc\) 值都相等。

#include<iostream>
#include<cstdio>
#include<vector>
#include<bits/extc++.h>
using namespace std;
using namespace __gnu_pbds;
#define MAXN 3005
#define pii pair<long long, long long> 
#define fi first
#define se second

long long read() {
	long long x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

long long n, m, dfn[MAXN], low[MAXN], dn, dcc[MAXN], cnt, ff[MAXN];
unsigned long long h[MAXN];
bool f[MAXN], vis[MAXN];
vector<pii > v[MAXN];

long long find(long long x) { return (ff[x] == x) ? x : ff[x] = find(ff[x]); }

void tarjan(long long x, long long id) {
	dfn[x] = low[x] = ++ dn;
	for(auto it : v[x]) {
		long long y = it.fi, d = it.se;
		if(vis[d]) continue;
		if(!dfn[y]) tarjan(y, d), low[x] = min(low[x], low[y]);
		else if(d != id) low[x] = min(low[x], dfn[y]);
	}
	if(dfn[x] == low[x] && id != -1) f[id] = 1;
	return;
}

void dfs(long long x, long long id, bool fg) {
	if(!fg) dcc[x] = id;
	else h[x] = h[x] * 233ull + id;
	for(auto it : v[x])
		if(!f[it.se] && !dcc[it.fi]) dfs(it.fi, id, fg);
	return;
}

long long calc(long long x, long long y) {
	if(find(x) != find(y)) return 0;
	if(dcc[x] != dcc[y]) return 1;
	if(h[x] == h[y]) return 3;
	return 2;
}

int main() {
	n = read(), m = read();
	for(int i = 1; i <= n; i ++) ff[i] = i;
	for(int i = 1; i <= m; i ++) {
		long long x = read(), y = read();
		v[x].push_back({y, i});
		v[y].push_back({x, i});
		ff[find(x)] = find(y);
	}
	for(int i = 1; i <= n; i ++) if(!dfn[i]) tarjan(i, -1);
	for(int i = 1; i <= n; i ++) if(!dcc[i]) dfs(i, ++ cnt, 0);
	for(int i = 1; i <= m; i ++) {
		vis[i] = 1, dn = cnt = 0;
		for(int j = 1; j <= n; j ++) dfn[j] = low[j] = 0;
		for(int j = 1; j <= m; j ++) f[j] = 0;
		for(int j = 1; j <= n; j ++) if(!dfn[j]) tarjan(j, -1);
		for(int i = 1; i <= n; i ++) if(!dcc[i]) dfs(i, ++ cnt, 1);
	}
	long long ans = 0;
	for(int i = 1; i <= n; i ++) for(int j = i + 1; j <= n; j ++) ans += calc(i, j);
	cout << ans << "\n";
	return 0;
}
posted @ 2026-01-13 20:56  So_noSlack  阅读(13)  评论(0)    收藏  举报