环的计数

创建时间:2025-10-09


环的计数是一个较为少见的问题,但也具有一定的重要性,是图论板块中必不可少的一部分。其中三元环计数是最基础的环计数问题。

P1989 无向图三元环计数 为例,给定 \(n\) 个点 \(m\) 条边的无向简单图 \(G\),求三元环数量。形式化的说,统计包含且仅包含三个点 \(u,v,w\) 和三条边 \(\langle u,v \rangle,\langle v,w\rangle,\langle w,u \rangle\) 的子图 \(G_0\) 的数量(两个三元环 \(G_1=\{V_1,E_1\}\)\(G_2=\{V_2,E_2\}\) 相同当且仅当 \(V_1=V_2\)\(E_1=E_2\))。

为了不重复计数,考虑给节点赋上唯一的优先级,比如随机数,设 \(i\) 优先级为 \(p_i\)\(\forall i\neq j,p_i \neq p_j\))。建一张的有向图新图 \(G'\),对于 \(G\) 中边 \(\langle u,v\rangle\),若 \(p_u<p_v\),则在 \(G'\) 中连 \(u \rightarrow v\),否则连 \(v \rightarrow u\)。显然,\(G'\) 为 DAG,因为 \(G'\) 有拓扑序,即 \(p\)

此时 \(G\) 中三元环 \(\langle u,v \rangle,\langle v,w\rangle,\langle w,u\rangle\) (图1),其中 \(p_u<p_v<p_w\),便能转化成 \(G'\)\(u\rightarrow v,v\rightarrow w,u\rightarrow w\)(图2)。故只需在 \(G'\) 中先枚举 \(u\),再枚举存在 \(u\rightarrow v\)\(v\),然后枚举存在 \(v\rightarrow w\)\(w\),最后检查是否存在 \(u\rightarrow w\) 即可。


代码:

for (int i = 1; i <= m; i++)
	if (pry(x[i]) < pry(y[i]))
		e[x[i]].push_back(y[i]);
	else
		e[y[i]].push_back(x[i]);
for (int i = 1; i <= n; i++) {
	for (int j : e[i])
		vis[j] = i;
	for (int j : e[i])
		for (int k : e[j])
			if (vis[k] == i)
				ans++;
}

显然,如果人品足够好,长得足够帅,把 \(p_i\) (代码中 \(pry(i)\))赋值为随机数是有可能过的,但在大部分情况下时间复杂度为 \(O(nm)\)

下面令 \(p_i=(deg_i,i)\),其中 \(deg_i\)\(i\)\(G\) 中度数,作为优先级的第一关键字,若 \(deg_i=deg_j\) 则比较 \(i\)\(j\) 的大小。即对于原图 \(G\) 中边 \((u,v)\),若 \(deg_u < deg_v\)\(deg_u = deg_v \vee u < v\),在 \(G'\) 中连边 \(u \rightarrow v\);否则,\(v \rightarrow u\)

首先证明在 \(G'\) 中任意一点的出边数量最多为 \(\sqrt {2m}\)

对于点 \(x\),如果 \(deg_x \le \sqrt {2m}\),即 \(x\) 在原图 \(G\) 中度数不超过 \(\sqrt {2m}\),则 \(x\)\(G'\) 中出度也一定不超过 \(\sqrt {2m}\)

如果 \(deg_x > \sqrt {2m}\),那么当 \(G'\)\(x \rightarrow y\) 时一定有 \(deg_y \ge deg_x > \sqrt {2m}\)。注意到 \(\sum_{i=1}^n deg_i = 2m\),所以 \(deg_y > \sqrt {2m}\)\(y\) 最多 \(\sqrt {2m}\) 个。

综上,在 \(G'\) 中任意一点出度最多为 \(\sqrt {2m}\),即为 \(O(\sqrt m)\) 级别。

又因为 \(G'\) 中一共有 \(m\) 条边,所以 \(u \rightarrow v\) 一共 \(m\) 条,由前面的推导知对于每个 \(v\)\(v \rightarrow w\) 最多 \(O(\sqrt m)\),所以算法的总时间复杂度为 \(O(m \sqrt m)\),且常数小。

容易发现,通过根号分治的思想和优先级的选择让暴力枚举的时间复杂度从 \(O(mn)\) 降到了 \(O(m\sqrt m)\)

完整代码:

#include <iostream>
#include <vector>

const int MAX_N = 1e5 + 50;
const int MAX_M = 2e5 + 50;

int n, m, ans, deg[MAX_N];
int x[MAX_M], y[MAX_M];
int vis[MAX_N];
vector<int> e[MAX_N];

#define pry(i) make_pair(deg[i], i) 

int main() {
	cin >> n >> m;
	for (int i = 1; i <= m; i++) {
		cin >> x[i] >> y[i];
		deg[x[i]]++, deg[y[i]]++;
	}
	for (int i = 1; i <= m; i++)
		if (pry(x[i]) < pry(y[i]))
			e[x[i]].push_back(y[i]);
		else
			e[y[i]].push_back(x[i]);
	for (int i = 1; i <= n; i++) {
		for (int j : e[i])
			vis[j] = i;
		for (int j : e[i])
			for (int k : e[j])
				if (vis[k] == i)
					ans++; 
	}
	cout << ans << '\n';
	return 0;
}

三元环计数只是环计数问题最基础的部分,考虑怎么扩展至四元环计数:U367189 无向图四元环计数,给定无向图 \(G\),求图中四元环数量。形式化的说,统计包含且仅包含四个点 \(a,b,c,d\) 和四条边 \(\langle a,b \rangle,\langle b,c\rangle,\langle c,d \rangle,\langle d,a\rangle\) 的子图 \(G_0\) 的数量(两个三元环 \(G_1=\{V_1,E_1\}\)\(G_2=\{V_2,E_2\}\) 相同当且仅当 \(V_1=V_2\)\(E_1=E_2\)

沿用三元环问题中的变量。

设有 \(G\) 的子图 \(G_0=\{V,E\}\),其中 \(V=\{a,b,c,d\},E=\{\langle a,b \rangle,\langle b,c\rangle,\langle c,d \rangle,\langle d,a\rangle\}\)。为了不重复计数,钦定 \(c\)\(a,b,c,d\) 中优先级最大的点,故 \(G_0\) 为四元环的充要条件为在 \(G'\) 中有边 \(b \rightarrow c\)\(d \rightarrow c\),并且 \(p_a<p_c\)

注意到此时 \(a\)\(c\) 是否有边是不确定的,\(p_a,p_b,p_d\) 之间的大小关系也是不确定的,所以条件有且仅有:\(G\) 中存在 \(\langle a,b\rangle\)\(\langle a,d\rangle\)\(G'\) 中存在 \(b\rightarrow c\)\(d\rightarrow c\)\(p_a<p_c\)

显然的是,\(b\)\(d\) 在这个问题中是等价的,即满足同样的条件,所以之际上只需要枚举其中一个,并利用先前得到的信息统计答案即可。具体来说:首先枚举 \(a\) 并枚举与 \(a\)\(G\) 相连的 \(b\),然后枚举 \(G'\)\(b\) 的出边 \(b\rightarrow c\),如果 \(p_a>p_c\) 果断跳过。设在当前 \(b\) 前面满足条件(包括 \(p_a<p_c\))的 \(c\)\(i\) 一共出现了 \(cnt_i\) 次,则先将 \(ans\) 加上 \(cnt_c\),再将 \(cnt_c\)\(1\)

写成代码如下:

#define pry(x) make_pair(e1[x].size(), x)

int main() {
	cin >> n >> m;
	for (int i = 1; i <= m; i++) {
		cin >> x[i] >> y[i];
		e1[x[i]].push_back(y[i]);
		e1[y[i]].push_back(x[i]);
	}
	for (int i = 1; i <= m; i++) {
		if (pry(x[i]) < pry(y[i]))
			e2[x[i]].push_back(y[i]);
		else
			e2[y[i]].push_back(x[i]);
	}
	long long ans = 0;
	for (int i = 1; i <= n; i++) {
		for (int j : e1[i])
			for (int k : e2[j])
				if (pry(i) < pry(k)) {
					ans += cnt[k];
					cnt[k]++;
				}
		for (int j : e1[i])
			for (int k : e2[j])
				if (pry(i) < pry(k))
					cnt[k] = 0;
	}
	cout << ans << '\n';
	return 0;
}

由算法步骤易证正确性,时间复杂度的分析方式也与三元环相同,同样是 \(O(m\sqrt m)\)

那么这种思路可以扩展至 \(k\) 元环吗?以 CF11D CF11D A Simple Task

理论上是可行的,但还按照这种思路不仅难写,时间复杂度也会爆炸。事实上针对这种问题还是状压

posted @ 2026-06-02 16:27  xubaichuan  阅读(8)  评论(0)    收藏  举报