环的计数
创建时间: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
理论上是可行的,但还按照这种思路不仅难写,时间复杂度也会爆炸。事实上针对这种问题还是状压

浙公网安备 33010602011771号