无向图三元环计数

无向图三元环计数

题目背景

无向图 $G$ 的三元环指的是一个 $G$ 的一个子图 $G_0$,满足 $G_0$ 有且仅有三个点 $u, v, w$,有且仅有三条边 $\langle u, v \rangle, \langle v, w \rangle, \langle w, u \rangle$。两个三元环 $G_1, G_2$ 不同当且仅当存在一个点 $u$,满足 $u \in G_1$ 且 $u \notin G_2$。

题目描述

给定一个 $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):$n \le 500$,$m \le {10}^3$。
  • Subtask 2(70 points):无特殊性质。

对于 $100\%$ 的数据,$1 \le n \le 10^5$,$1 \le m \le 2 \times {10}^5$,$1 \le u, v \le n$,给出的图不存在重边和自环,但不保证图连通

【提示】

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

 

解题思路

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

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

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

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

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

  AC 代码如下,时间复杂度为 $O\left(\frac{nm}{w}\right)$:

#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 中一定有 $u \to v$,$v \to w$,$u \to w$。所以枚举 $u$ 指向的 $v$,再枚举 $v$ 指向的 $w$,最后判断 $u$ 是否指向 $w$ 即可确定一个三元环。该做法的时间复杂度是 $O(m \sqrt{m})$,下面给出证明。

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

  AC 代码如下,时间复杂度为 $O(m \sqrt{m})$:

#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 @ 2024-08-22 17:50  onlyblues  阅读(24)  评论(0编辑  收藏  举报
Web Analytics