最小环 最小环计数

https://oi-wiki.org/graph/min-cycle/

最小环又分为:

  1. 有向带权图最小环
  2. 无向带权图最小环
  3. 无权图最小环

下面这篇文章很好,先看完再看下面内容
https://blog.csdn.net/qq_30320171/article/details/130275569

有向带权图最小环

https://codeforces.com/gym/103409/problem/E

好题,思路其实非常简洁——鼓励你先把直觉想清楚:不管 Alice 买什么边,Bob 最多也只需要 2 轮就能把所有边删完(当然 Alice 也可以选择不买,轮数为 0)。


关键观察

  • 若 Alice 不买任何边,结果就是 0 轮。
  • 若 Alice 买的所有边构成了一个有向无环图(DAG),那么 Bob 一轮就能删完(因为把这些边按任意拓扑序都向前的一组就能一次删完)。所以在这种情况下答案为 1。
  • 若 Alice 买的边包含 至少一个有向回路(至少存在一个有向环),那么 Bob 至少需要 2 轮;而事实上 任意有向图的边都可以按单一顶点全序(任意全序)分成两类:前向边和反向边,这两类都不会产生有向环,因此 Bob 最多用 2 轮就能删完。因此当 Alice 能买出一个环时,答案就是 2。

所以结论:最终答案只可能是 \(0,1,2\) 。Alice 会尽量让轮数最大化,所以:

  • 若存在一个环的边集总价 \(\le c\) ,Alice 能买出一个环,答案 \(=2\)
  • 否则若存在至少一条边价格 \(\le c\) ,Alice 能买一条边(虽然不能形成环),答案 \(=1\)
  • 否则 Alice 什么也买不了,答案 \(=0\)

因此任务变为:判断最小费用的有向环的总权重是否 \(\le c\) ,以及是否存在单条边价 \(\le c\)


如何求「最小权重的有向环」?

常见做法(权值非负的情形):

对每个顶点 \(s\) 做一次 Dijkstra(以 \(s\) 为起点,计算 \(s\) 到其它所有点的最短路),然后检查所有以 \(u\to s\) 的边(终点为 \(s\) ):

  • 若从 \(s\)\(u\) 有一条路径, 那么这条路径加上边 \(u\to s\) 就构成一个环,环权重为 \(\text{dist}(s\to u) + w(u\to s)\)
    对所有 \(s\) 和所有入边取最小值即为最短有向环的权重。

复杂度:做 \(n\) 次 Dijkstra,每次 \(O(m\log n)\) ,总体约为 \(O(n\cdot m\log n)\) 。在本题范围 \(n\le 2000, m\le 5000\) 是可行的(注意实现要高效)。


时间复杂度与空间

  • 时间: \(O(n\cdot (m\log n))\) (每个顶点一次 Dijkstra)
  • 空间: \(O(n+m)\)

C++ 实现(OI 风格,详细注释)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = (ll)9e18;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int n, m;
    ll c;
    if(!(cin >> n >> m >> c)) return 0;
    
    // 邻接表(正向),同时保存入边列表方便在每次以 s 为起点的 Dijkstra 后检查入边
    vector<vector<pair<int,int>>> adj(n+1);
    vector<vector<pair<int,int>>> in_edges(n+1); // in_edges[v] 存 (u, weight) 表示 u->v
    bool existAffordableEdge = false; // 是否存在单条边价格 <= c
    for(int i=0;i<m;i++){
        int u,v;
        int p;
        cin >> u >> v >> p;
        adj[u].push_back({v,p});
        in_edges[v].push_back({u,p});
        if((ll)p <= c) existAffordableEdge = true;
    }
    
    // 如果没有任何单条边能买,Alice 不能买任何边 -> 0 轮
    if(!existAffordableEdge){
        cout << 0 << '\n';
        return 0;
    }
    
    // 现在需要判断是否存在总价 <= c 的有向环(最短有向环权 <= c)
    // 对每个 s 做 Dijkstra(以 s 为起点,计算 s->* 的最短路),然后检查所有入边 u->s
    ll minCycle = INF;
    // Dijkstra 的数据结构预先分配,减少每次分配开销
    vector<ll> dist(n+1);
    vector<char> vis(n+1);
    for(int s=1;s<=n;s++){
        // 初始化
        fill(dist.begin(), dist.end(), INF);
        fill(vis.begin(), vis.end(), 0);
        dist[s] = 0;
        // 优先队列 (dist, node),小顶堆
        priority_queue<pair<ll,int>, vector<pair<ll,int>>, greater<pair<ll,int>>> pq;
        pq.push({0,s});
        while(!pq.empty()){
            auto cur = pq.top(); pq.pop();
            int u = cur.second;
            ll d = cur.first;
            if(vis[u]) continue;
            vis[u] = 1;
            // 如果当前距离已经 >= minCycle,则继续扩展没有意义(剪枝)
            if(d >= minCycle) break;
            for(auto &e : adj[u]){
                int v = e.first;
                int w = e.second;
                if(dist[v] > d + w){
                    dist[v] = d + w;
                    pq.push({dist[v], v});
                }
            }
        }
        // 检查所有指向 s 的边 u->s
        for(auto &e : in_edges[s]){
            int u = e.first;
            int w = e.second;
            if(dist[u] == INF) continue; // s 到 u 不可达 -> 无环通过这条边
            // s -> ... -> u  +  (u -> s) 构成一个环
            ll cycleCost = dist[u] + (ll)w;
            if(cycleCost < minCycle) minCycle = cycleCost;
        }
    }
    
    if(minCycle <= c){
        cout << 2 << '\n';
    }else{
        // 否则不能构成预算内的环,但我们已知存在可买的单条边 -> 1
        cout << 1 << '\n';
    }
    return 0;
}

小结

  • 关键观察是把游戏转换成「是否存在有向回路」的问题,Bob 能把任意有向图的边分两次删除;因此答案只在 \(\{0,1,2\}\) 中。
  • 你已经具备了实现思路:求最短有向环(多次 Dijkstra),然后比较与预算 \(c\)
  • 代码里有剪枝(在 Dijkstra 中当当前距离 \(\ge\) 全局最小环权时可结束扩展),有助于在某些实例上加速。

P63002 [ CSP-S 三十连测第一套 ]--T2--最小环计数
https://www.mxoj.net/problem/P63002
找到原题是这题:
https://codeforces.com/gym/104030/problem/E

官方题解大概是这样:

#include <bits/stdc++.h>
using namespace std;

const int INF = 1e9;
const int N = 3005;   // 最大点数 3000
vector<int> g[N];     // 邻接表存图
int n, m;

// ------------------ Step 1: 求最小环长度 k ------------------ //
int girth = INF;  // 最小环长度

void find_girth() {
    for (int s = 1; s <= n; s++) {
        vector<int> dist(n + 1, INF);
        vector<int> pre(n + 1, -1);
        queue<int> q;

        dist[s] = 0;
        q.push(s);

        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (int v : g[u]) {
                if (dist[v] == INF) {
                    // BFS 树边
                    dist[v] = dist[u] + 1;
                    pre[v] = u;
                    q.push(v);
                } else if (pre[u] != v) {
                    // 遇到非树边,发现一个环
                    girth = min(girth, dist[u] + dist[v] + 1);
                }
            }
        }
    }
}

// ------------------ Step 2: 统计最小环个数 ------------------ //
// 注意:每个环会被数 k 次(环上每个点做根都能数到一次),所以最后要除以 k
long long count_cycles() {
    long long total = 0;

    for (int s = 1; s <= n; s++) {
        vector<int> dist(n + 1, INF);
        vector<long long> ways(n + 1, 0);  // ways[v]: s 到 v 的最短路条数
        queue<int> q;

        dist[s] = 0;
        ways[s] = 1;
        q.push(s);

        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (int v : g[u]) {
                if (dist[v] == INF) {
                    // 首次到达,更新距离和路径数
                    dist[v] = dist[u] + 1;
                    ways[v] = ways[u];
                    q.push(v);
                } else if (dist[v] == dist[u] + 1) {
                    // 另一条最短路
                    ways[v] += ways[u];
                }
            }
        }

        if (girth % 2 == 0) {
            // ------------ Case 1: girth 偶数 ------------
            int d = girth / 2;
            for (int v = 1; v <= n; v++) {
                if (dist[v] == d) {
                    // 每个点的最短路条数 ways[v]
                    // 组合数 C(ways[v], 2) = ways[v] * (ways[v]-1) / 2
                    total += ways[v] * (ways[v] - 1) / 2;
                }
            }
        } else {
            // ------------ Case 2: girth 奇数 ------------
            int d = (girth - 1) / 2;
            for (int u = 1; u <= n; u++) {
                if (dist[u] == d) {
                    for (int v : g[u]) {
                        if (dist[v] == d && u < v) {
                            // 边 (u,v) 是中间层的边
                            total += ways[u] * ways[v];
                        }
                    }
                }
            }
        }
    }

    // 每个环数了 girth 次
    return total / girth;
}

// ------------------ 主函数 ------------------ //
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }

    find_girth();

    if (girth == INF) {
        cout << 0 << "\n";  // 图中没有环
        return 0;
    }

    cout << count_cycles() << "\n";
    return 0;
}

别人题解:

#include<bits/stdc++.h> 
#pragma GCC optimize(3)  // 开启编译优化,提高运行速度
using namespace std;
typedef long long ll;

int n, m;                       // n = 点数,m = 边数
vector<int> t[3006];            // 邻接表存图
int dis[3006], ans, cnt[3005];  // dis[]: BFS距离,cnt[]: 最短路条数,ans: 当前最小环长度
int q[6006], fr, ba;            // 手写队列:q[]存节点编号,fr队首,ba队尾

// bfs(c, y):从点 c 出发做 BFS,同时“禁止”使用边 (c, y)
void bfs(int c,int y){
    memset(dis, -1, sizeof(dis));   // 初始化距离为 -1(表示未访问)
    memset(cnt, 0, sizeof(cnt));    // 初始化最短路条数为 0
    
    dis[c] = 0;                     // 起点 c 到自己的距离为 0
    fr = ba = 1; q[1] = c;          // 初始化队列,只有起点 c
    int x = c; cnt[c] = 1;          // cnt[c]=1,起点有 1 条路径到自己

    while(fr <= ba){                // BFS 遍历队列
        c = q[fr++];                // 出队
        for(auto i : t[c]){         // 枚举邻居 i
            // 如果当前边是被禁止的 (x, y),则跳过
            if(!(c == x && i == y)){
                if(dis[i] == -1){   // i 还没访问过
                    dis[i] = dis[c] + 1;   // 更新距离
                    cnt[i] = cnt[c];       // 最短路条数继承自 c
                    q[++ba] = i;           // 入队
                }
                else if(dis[i] == dis[c] + 1){
                    // 找到另一条最短路径到 i
                    cnt[i] += cnt[c];
                }
            }
        }
    }
}

int x[6005], y[6005]; // 存输入的边
long long sum;        // 统计所有最小环的条数(可能有重复,需要除以环长)

int main(){
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);

    cin >> n >> m;
    ans = m + 10;   // 初始化最小环长度为一个大数

    // 读入边,建无向图
    for(int i = 1; i <= m; ++i){
        cin >> x[i] >> y[i];
        t[x[i]].emplace_back(y[i]);
        t[y[i]].emplace_back(x[i]);
    }

    // 枚举每条边 (x[i], y[i]),尝试找包含这条边的最小环
    for(int i = 1; i <= m; ++i){
        bfs(x[i], y[i]);          // 从 x[i] 出发,禁止边 (x[i], y[i])
        
        if(dis[y[i]] == -1) continue;   // 如果 y[i] 不可达,则不存在环

        // 当前找到的环长度 = dis[y[i]] + 1 (再加上被禁止的那条边)
        if(ans > dis[y[i]] + 1){
            // 找到更短的环 → 更新答案
            sum = cnt[y[i]];          // 记录条数
            ans = dis[y[i]] + 1;      // 更新最小环长度
        }
        else if(ans == dis[y[i]] + 1){
            // 找到同样长度的最小环 → 叠加条数
            sum += cnt[y[i]];
        }
    }

    // 每个环在统计时被算了 ans 次(因为环长 = ans,每条边都可能作为“被禁止的边”)
    cout << sum / ans;

    return 0;
}

📝 思路总结

  1. 核心技巧
    • 枚举每条边 \((u, v)\) ,临时删去它。
    • 然后做 BFS,看从 \(u\)\(v\) 的最短路是多少。
    • 加上那条边,得到一个环。
  2. 为什么正确?
    • 任何最小环至少包含一条边。
    • 当我们枚举到这条边时,必然能通过 BFS 找到环长。
    • 因此不会漏。
  3. 计数方法
    • cnt[v] 记录 BFS 过程中最短路条数。
    • 如果 u → ... → v 有多条最短路,就说明对应的环也有多种形态。
    • 但由于每个环被计算了 环长 次,所以最终要除以 ans

✅ 所以这份代码就是用 删边 BFS 方法,既能求最小环长度,又能数出最小环的个数。

posted @ 2025-08-25 17:52  katago  阅读(10)  评论(0)    收藏  举报