最小环计数问题

写在前面:无向图上的简单环计数问题是一个NP-Hard问题,一般情况下不会考察,也不会要求输出图上的所有环。对于图论中环上的问题,一般都是给定某种性质,然后根据这种性质通过并查集/搜索等手段解决问题;或者更基础的,可能会要求我们判断图上是否存在且仅存在一个环(使用并查集或者dfn序)。
做一个简单总结:
DFS、BFS、DSU都可以判断图上是否有环或是否仅有一个环;
BFS可以求最小环的长度和最小环的数量(需要枚举所有点,时间复杂度较高);
DFS可以判断图上是否仅有一环且求出该环的长度。

题目链接:E. Enigmatic Enumeration

思路:题目非常裸,就是要求无向图上所有最小环的数量。

首先我们能很自然地想到要先求出“最小环”的长度到底是多少,枚举每个点(枚举所有点的原因是,我们需要保证从环上的点开始枚举,才能求出最小环的长度),然后求包含当前点的最小环的长度。这里使用BFS的方式进行求解,大概是笔者写法原因,DFS的方式无法正确求解,不过也有可能与DFS的遍历方式与深度优先的特性有关。

由于我们存的是无向图,所以需要一个类似于DFS搜索中如下的处理方式。

for (auto y : adj[x]) {
    if (y == parent) {
        continue;
    }
}

这里BFS搜索中的处理方式个人感觉值得学习

for (auto y : adj[x]) {
    if (y == p) {
        continue;
    }
    if (dis[y] != -1) {
        if (dis[y] >= dis[x]) {
			work();
        }
        continue;
    }
    q.push({y, x});
    dis[y] = dis[x] + 1;
}

代码中的work()函数是指某些操作,那么其实dis[y] >= dis[x]的作用就是,避免了重新回到父节点。

实际上这种写法还有其他妙处,等下再展开描述。

此时我们已经求好最小环的长度。然后,就是再次对每个点进行搜索,求出有多少环的长度等于最小环长。读者可能认为只要重复上述过程然后加判断是否相等的条件就可以了,但仔细考虑你会发现,由于搜索的原因,会导致同一个环重复计数(读者可以通过对三个点、四个点的环手玩一下)。

于是我们考虑另一种可行的方案,就是将环拆开,利用边的条数进行求解,接下来进行解释:

不妨设环长为len,对于当前搜索到的点与枚举的点所构成的线段,若边长为l1的条数为n,边长为len - l1的条数为m,那么根据乘法原理,我们能得到环长为len的环的数量有n * m个。

具体来说,对于偶数环的情况,我们记录距离当前正枚举的点的距离为len/2的点的边数即可。

对于奇数环,我们记录距离当前正枚举的点的距离为(len - 1) / 2的边的边数即可。

代码实现也大致如此,只是需要注意,由于BFS的特点,x与y点的dis值不会超过1,所以对于偶数环的情况需要进行一个特殊处理。

也即,当环的长度为偶数时,找到dis[y] == dis[x] + 1的点时,y的路径数也需要累加上x的路径数,这里dis[y]的值实际上就是len/2。其实到这里,上面所说的妙处也就体现出来了,一定程度避免了重复计算的情况。

对于环的长度为奇数时,环的数量会被重复计算一次,可以对三个点的环手模一下,所以最终需要除以2。

对于所有情况,由于环上每个点都会被枚举一次,所以计数除以环长即为答案。

#include <bits/stdc++.h>

using i64 = long long;

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n, m;
    std::cin >> n >> m;

    std::vector<std::vector<int>> adj(n);
    for (int i = 0; i < m; i++) {
        int u, v;
        std::cin >> u >> v;
        u--, v--;

        adj[u].push_back(v);
        adj[v].push_back(u);
    }

    int minDis = 1E9;
    for (int i = 0; i < n; i++) {
        std::queue<int> q;
        std::vector<int> dis(n, -1);
        dis[i] = 0;
        q.push(i);

        while (!q.empty()) {
            int x = q.front();
            q.pop();

            for (auto y : adj[x]) {
                if (dis[y] != -1) {
                    if (dis[y] >= dis[x]) {
                        minDis = std::min(minDis, dis[x] + dis[y] + 1);
                    }
                    continue;
                }

                q.push(y);
                dis[y] = dis[x] + 1;
            }
        }
    }   

    int ans = 0;
    for (int i = 0; i < n; i++) {
        std::queue<int> q;
        std::vector<int> dis(n, -1), cnt(n);
        dis[i] = 0;
        cnt[i] = 1;
        q.push(i);

        while (!q.empty()) {
            int x = q.front();
            q.pop();

            for (auto y : adj[x]) {
                if (dis[y] != -1) {
                    if (dis[y] >= dis[x] && dis[x] + dis[y] + 1 == minDis) {
                        ans += cnt[y];

                        if (minDis % 2 == 0) {
                            cnt[y] += cnt[x];
                        }
                    }
                    continue;
                }

                q.push(y);
                dis[y] = dis[x] + 1;
                cnt[y] = cnt[x];
            }
        }

    }

    // debug(ans);
    if (minDis % 2) {
        ans /= 2;
    }

    ans /= minDis;
    std::cout << ans << "\n";
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值