无向图三元环计数
无向图三元环计数
题目背景
无向图 的三元环指的是一个 的一个子图 ,满足 有且仅有三个点 ,有且仅有三条边 。两个三元环 不同当且仅当存在一个点 ,满足 且 。
题目描述
给定一个 个点 条边的简单无向图,求其三元环个数。
输入格式
每个测试点有且仅有一组测试数据。
输入的第一行是用一个空格隔开的两个整数,分别代表图的点数 和边数 。
第 到第 行,每行两个用空格隔开的整数 ,代表有一条连接节点 和节点 的边。
输出格式
输出一行一个整数,代表该图的三元环个数。
样例 #1
样例输入 #1
3 3
1 2
2 3
3 1
样例输出 #1
1
样例 #2
样例输入 #2
5 8
1 2
2 3
3 5
5 4
4 2
5 2
1 4
3 4
样例输出 #2
5
提示
【样例 2 解释】
共有 个三元环,每个三元环包含的点分别是 。
【数据规模与约定】
本题采用多测试点捆绑测试,共有两个子任务。
- Subtask 1(30 points):,。
- Subtask 2(70 points):无特殊性质。
对于 的数据,,,,给出的图不存在重边和自环,但不保证图连通。
【提示】
- 请注意常数因子对程序效率造成的影响。
解题思路
先给出根号分治的暴力做法。
对于一个三元环 ,我们可以先枚举边 ,然后枚举 (或 )的邻点 ,若 也是 (或 )的邻点,那么就得到三元环 了。如果是菊花图的话会被卡到 。由于我们只关心同时与 和 相邻的点的个数,为此我们可以给每个点开一个 std::bitset
标记其邻点,那么两个节点对应的 std::bitset
进行与运算后 的个数就是共同相邻点的个数。但空间复杂度是 。
这时候就可以根号分治了,设一个阈值 ,度数超过 的节点定义为重节点,否则为轻节点。容易知道重节点的数量不超过 ,为此我们可以给每个重节点开一个 std::bitset
标记其邻点,空间复杂度为 。根据边 两端点的种类不同有相应的枚举策略,不失一般性假设 的度数不超过 的度数:
- 若 的度数超过 ,意味着 和 都是重节点,直接将
std::bitset
进行与运算。时间复杂度为 。 - 否则 的度数不超过 ,
- 的度数也不超过 ,直接暴力枚举 的邻点 并判断 是否与 相邻。时间复杂度为 或 (取决于用什么容器存储邻点)。
- 的度数超过 ,意味着 是重节点,暴力枚举 的邻点 并判断 对应
std::bitset
的第 位是否被标记。时间复杂度为 。
最后代码中 的取值设为 ,是可以过的。还有需要注意的是该方法每个三元环会被统计 次,最后答案需要除以 。
AC 代码如下,时间复杂度为 :
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 5, M = 1500;
int x[N], y[N];
vector<int> g[N];
bool vis[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
for (int i = 1; i <= m; i++) {
cin >> x[i] >> y[i];
g[x[i]].push_back(y[i]);
g[y[i]].push_back(x[i]);
}
map<int, bitset<N>> mp;
for (int i = 1; i <= n; i++) {
if (g[i].size() > M) {
for (auto &j : g[i]) {
mp[i][j] = 1;
}
}
}
LL ret = 0;
for (int i = 1; i <= m; i++) {
if (g[x[i]].size() > g[y[i]].size()) swap(x[i], y[i]);
if (g[x[i]].size() <= M) {
if (g[y[i]].size() <= M) {
for (auto &v : g[y[i]]) {
vis[v] = true;
}
for (auto &v : g[x[i]]) {
if (vis[v]) ret++;
}
for (auto &v : g[y[i]]) {
vis[v] = false;
}
}
else {
auto &p = mp[y[i]];
for (auto &v : g[x[i]]) {
if (p[v]) ret++;
}
}
}
else {
ret += (mp[x[i]] & mp[y[i]]).count();
}
}
cout << ret / 3;
return 0;
}
再给出更高效的做法,正常情况下是想不到的。
给每条边确定一个方向,规定度数小的节点指向度数大的节点,如果两个节点的度数相等,则编号小的节点指向编号大的节点。最后得到的是一个有向无环图,否则如果有环的话,那么必定会与前面的定义产生矛盾。
对于三元环 ,不失一般性假设三个节点的度数依次递增,度数相同则编号依次递增,那么在 DAG 中一定有 ,,。所以枚举 指向的 ,再枚举 指向的 ,最后判断 是否指向 即可确定一个三元环。该做法的时间复杂度是 ,下面给出证明。
考虑每条边对复杂度的贡献,其实就是每个点的入度乘以出度,可以表示为 ,其中 指第 条边被指向的点, 指点 的入度。分情况讨论,如果在初始的无向图中点 的度数不超过 ,那么在 DAG 中其出度也不会超过 ,这样的点最多有 个,因此复杂度就是 。如果在初始的无向图中点 的度数超过 ,由于图中度数超过 的点最多有 个,因此复杂度就是 。
AC 代码如下,时间复杂度为 :
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2e5 + 5;
int x[N], y[N];
int deg[N];
vector<int> g[N];
bool vis[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
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 (deg[x[i]] < deg[y[i]] || deg[x[i]] == deg[y[i]] && x[i] < y[i]) g[x[i]].push_back(y[i]);
else g[y[i]].push_back(x[i]);
}
LL ret = 0;
for (int i = 1; i <= n; i++) {
for (auto &j : g[i]) {
vis[j] = true;
}
for (auto &j : g[i]) {
for (auto &k : g[j]) {
if (vis[k]) ret++;
}
}
for (auto &j : g[i]) {
vis[j] = false;
}
}
cout << ret;
return 0;
}
参考资料
题解 P1989 【【模板】无向图三元环计数】 - 洛谷专栏:https://www.luogu.com.cn/article/oz2l2vl2
环计数问题 - OI Wiki:https://oi-wiki.org/graph/rings-count/
本文来自博客园,作者:onlyblues,转载请注明原文链接:https://www.cnblogs.com/onlyblues/p/18374420
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效