无向图三元环计数

无向图三元环计数

题目背景

无向图 G 的三元环指的是一个 G 的一个子图 G0,满足 G0 有且仅有三个点 u,v,w,有且仅有三条边 u,v,v,w,w,u。两个三元环 G1,G2 不同当且仅当存在一个点 u,满足 uG1uG2

题目描述

给定一个 n 个点 m 条边的简单无向图,求其三元环个数。

输入格式

每个测试点有且仅有一组测试数据。

输入的第一行是用一个空格隔开的两个整数,分别代表图的点数 n 和边数 m

2 到第 (m+1) 行,每行两个用空格隔开的整数 u,v,代表有一条连接节点 u 和节点 v 的边。

输出格式

输出一行一个整数,代表该图的三元环个数。

样例 #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 解释】

共有 5 个三元环,每个三元环包含的点分别是 {1,2,4},{2,3,4},{2,3,5},{2,4,5},{3,4,5}

【数据规模与约定】

本题采用多测试点捆绑测试,共有两个子任务

  • Subtask 1(30 points):n500m103
  • Subtask 2(70 points):无特殊性质。

对于 100% 的数据,1n1051m2×1051u,vn,给出的图不存在重边和自环,但不保证图连通

【提示】

  • 请注意常数因子对程序效率造成的影响。

 

解题思路

  先给出根号分治的暴力做法。

  对于一个三元环 (u,v,w),我们可以先枚举边 (u,v),然后枚举 u(或 v)的邻点 w,若 w 也是 v(或 u)的邻点,那么就得到三元环 (u,v,w) 了。如果是菊花图的话会被卡到 O(m2)。由于我们只关心同时与 uv 相邻的点的个数,为此我们可以给每个点开一个 std::bitset 标记其邻点,那么两个节点对应的 std::bitset 进行与运算后 1 的个数就是共同相邻点的个数。但空间复杂度是 O(n2)

  这时候就可以根号分治了,设一个阈值 B,度数超过 B 的节点定义为重节点,否则为轻节点。容易知道重节点的数量不超过 O(mB),为此我们可以给每个重节点开一个 std::bitset 标记其邻点,空间复杂度为 O(mBn)。根据边 (u,v) 两端点的种类不同有相应的枚举策略,不失一般性假设 u 的度数不超过 v 的度数:

  • u 的度数超过 B,意味着 uv 都是重节点,直接将 std::bitset 进行与运算。时间复杂度为 O(nw)
  • 否则 u 的度数不超过 B
    • v 的度数也不超过 B,直接暴力枚举 u 的邻点 w 并判断 v 是否与 w 相邻。时间复杂度为 O(B)O(Blogn)(取决于用什么容器存储邻点)。
    • v 的度数超过 B,意味着 v 是重节点,暴力枚举 u 的邻点 w 并判断 v 对应 std::bitset 的第 w 位是否被标记。时间复杂度为 O(B)

  最后代码中 B 的取值设为 nw,是可以过的。还有需要注意的是该方法每个三元环会被统计 3 次,最后答案需要除以 3

  AC 代码如下,时间复杂度为 O(nmw)

#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;
}

  再给出更高效的做法,正常情况下是想不到的。

  给每条边确定一个方向,规定度数小的节点指向度数大的节点,如果两个节点的度数相等,则编号小的节点指向编号大的节点。最后得到的是一个有向无环图,否则如果有环的话,那么必定会与前面的定义产生矛盾。

  对于三元环 (u,v,w),不失一般性假设三个节点的度数依次递增,度数相同则编号依次递增,那么在 DAG 中一定有 uvvwuw。所以枚举 u 指向的 v,再枚举 v 指向的 w,最后判断 u 是否指向 w 即可确定一个三元环。该做法的时间复杂度是 O(mm),下面给出证明。

  考虑每条边对复杂度的贡献,其实就是每个点的入度乘以出度,可以表示为 i=1mdvi,其中 vi 指第 i 条边被指向的点,dv 指点 v 的入度。分情况讨论,如果在初始的无向图中点 u 的度数不超过 m,那么在 DAG 中其出度也不会超过 O(m),这样的点最多有 O(m) 个,因此复杂度就是 O(mm)。如果在初始的无向图中点 u 的度数超过 m,由于图中度数超过 m 的点最多有 O(m) 个,因此复杂度就是 O(mm)

  AC 代码如下,时间复杂度为 O(mm)

#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/

posted @   onlyblues  阅读(47)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
Web Analytics
点击右上角即可分享
微信分享提示