UVa 12544 Beehives

题目描述

蜜蜂是最勤劳的昆虫之一。由于它们从花朵中采集花蜜和花粉,它们必须依赖森林中的树木。为简化问题,它们将森林中的 nnn 棵树编号为 000n−1n-1n1。它们不使用森林中所有的路径,而是使用特定的路径列表。一条路径连接两棵树,并且它们可以双向移动,即从一棵树直线移动到另一棵树。它们不使用不在列表中的路径。

随着科技的巨大进步,它们也改变了工作策略。它们不再在森林中所有树木上盘旋,而是针对特定的树木,主要是那些有很多花的树。因此,它们计划在一些目标树木上建造新的蜂巢。之后,它们将只从这些树上采集食物。它们还将从列表中移除一些路径,这样就不必前往没有蜂巢的树。

现在,它们希望建造蜂巢,使得即使新列表中的某条路径出现问题(例如某些鸟类或动物干扰该路径),仍然可以通过现有路径从任意一个蜂巢到达另一个蜂巢。

它们不想选择少于两棵树,并且由于建造蜂巢需要大量工作,它们需要尽可能减少蜂巢的数量。现在,给定树木和它们使用的路径,你的任务是提出一个新的蜂巢群方案。

输入格式

输入以整数 TTTT≤50T \leq 50T50)开始,表示测试用例的数量。

每个测试用例以一个空行开始。下一行包含两个整数 nnn2≤n≤5002 \leq n \leq 5002n500)和 mmm0≤m≤200000 \leq m \leq 200000m20000),其中 nnn 表示树木的数量,mmm 表示路径的数量。接下来的 mmm 行每行包含两个整数 uuuvvv0≤u,v<n0 \leq u, v < n0u,v<nu≠vu \neq vu=v),表示树 uuuvvv 之间有一条路径。假设树 uuuvvv 之间最多只有一条路径,并且同一条路径不会在输入中重复出现。

输出格式

对于每个测试用例,输出用例编号和提议的蜂巢群中的蜂巢数量,如果无法找到这样的蜂巢群,则输出 impossible

注意: 数据集很大,请使用更快的 I/O\texttt{I/O}I/O 方法。

题目分析

问题本质

本题要求找到一个最小的顶点集合(蜂巢),满足以下条件:

  1. 顶点数量至少为 222
  2. 这些顶点构成的子图是边双连通
  3. 顶点数量尽可能小

边双连通图是指删除任意一条边后,图仍然保持连通的图。在边双连通图中,任意两个顶点之间至少存在两条边不相交的路径。

关键观察

  1. 边双连通分量:一个边双连通分量本身就是一个边双连通子图
  2. 最小边双连通子图:我们可以在边双连通分量中选择更小的边双连通子图
  3. 环的性质:一个环就是一个边双连通子图,最小的环就是最小的边双连通子图
  4. 特殊情况:如果边双连通分量中没有环但至少有一条边,那么两个顶点就构成最小的边双连通子图

算法思路

基于以上观察,我们的解题思路如下:

  1. 寻找边双连通分量

    • 使用 Tarjan\texttt{Tarjan}Tarjan 算法找出所有的桥(割边)
    • 通过 DFS\texttt{DFS}DFS 遍历跳过桥边,找到所有的边双连通分量
  2. 在每个分量中寻找最小环

    • 对于每个大小至少为 222 的边双连通分量
    • 在该分量对应的子图中使用 BFS\texttt{BFS}BFS 寻找最小环
    • 如果分量中没有环但大小 ≥2\geq 22,说明至少有一条边,答案为 222
  3. 确定最终答案

    • 取所有分量中最小环长度的最小值
    • 如果没有满足条件的分量,输出 impossible

算法复杂度

  • 找桥O(n+m)O(n + m)O(n+m)
  • 找边双连通分量O(n+m)O(n + m)O(n+m)
  • 找最小环:最坏情况下 O(n×(n+m))O(n \times (n + m))O(n×(n+m))
  • 总体复杂度:O(n×(n+m))O(n \times (n + m))O(n×(n+m)),在 n≤500n \leq 500n500 的范围内是可接受的

代码实现

// Beehives
// UVa ID: 12544
// Verdict: Accepted
// Submission Date: 2025-11-19
// UVa Run Time: 0.620s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net

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

const int MAXN = 505;
const int INF = 1e9;

vector<int> graph[MAXN];
int low[MAXN], dfn[MAXN], dfsClock;
bool isBridge[MAXN][MAXN];
vector<int> comp;
int n, m;

void findBridges(int u, int p) {
    low[u] = dfn[u] = ++dfsClock;
    for (int v : graph[u]) {
        if (v == p) continue;
        if (!dfn[v]) {
            findBridges(v, u);
            low[u] = min(low[u], low[v]);
            if (low[v] > dfn[u]) {
                isBridge[u][v] = isBridge[v][u] = true;
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}

void dfsComp(int u) {
    comp.push_back(u);
    dfn[u] = 1;
    for (int v : graph[u]) {
        if (!dfn[v] && !isBridge[u][v]) {
            dfsComp(v);
        }
    }
}

int findMinCycleInComponent(const vector<int>& component) {
    if (component.size() < 2) return INF;
    
    // 构建子图
    unordered_map<int, int> idMap;
    int compSize = component.size();
    for (int i = 0; i < compSize; ++i) {
        idMap[component[i]] = i;
    }
    
    vector<vector<int>> compGraph(compSize);
    for (int u : component) {
        for (int v : graph[u]) {
            if (idMap.count(v)) {
                compGraph[idMap[u]].push_back(idMap[v]);
            }
        }
    }
    
    // 在子图中找最小环
    int minCycle = INF;
    for (int start = 0; start < compSize; ++start) {
        vector<int> dist(compSize, -1);
        vector<int> parent(compSize, -1);
        queue<int> q;
        
        dist[start] = 0;
        q.push(start);
        
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            
            for (int v : compGraph[u]) {
                if (dist[v] == -1) {
                    dist[v] = dist[u] + 1;
                    parent[v] = u;
                    q.push(v);
                } else if (parent[u] != v) {
                    minCycle = min(minCycle, dist[u] + dist[v] + 1);
                }
            }
        }
    }
    
    return minCycle;
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(nullptr);
    
    int T;
    cin >> T;
    for (int caseNum = 1; caseNum <= T; ++caseNum) {
        cin >> n >> m;
        for (int i = 0; i < n; ++i) {
            graph[i].clear();
            dfn[i] = 0;
            for (int j = 0; j < n; ++j) {
                isBridge[i][j] = false;
            }
        }
        
        for (int i = 0; i < m; ++i) {
            int u, v;
            cin >> u >> v;
            graph[u].push_back(v);
            graph[v].push_back(u);
        }
        
        // 找桥
        dfsClock = 0;
        for (int i = 0; i < n; ++i) {
            if (!dfn[i]) {
                findBridges(i, -1);
            }
        }
        
        // 找边双连通分量
        memset(dfn, 0, sizeof(dfn));
        int answer = INF;
        
        for (int i = 0; i < n; ++i) {
            if (!dfn[i]) {
                comp.clear();
                dfsComp(i);
                
                if (comp.size() >= 2) {
                    int minCycle = findMinCycleInComponent(comp);
                    if (minCycle == INF) {
                        // 如果没有环但是有边,答案是2
                        answer = min(answer, 2);
                    } else {
                        answer = min(answer, minCycle);
                    }
                }
            }
        }
        
        cout << "Case " << caseNum << ": ";
        if (answer == INF) {
            cout << "impossible\n";
        } else {
            cout << answer << "\n";
        }
    }
    
    return 0;
}

总结

本题的关键在于理解边双连通图的性质和最小环的寻找方法。通过将问题分解为寻找边双连通分量和在分量中寻找最小环两个步骤,我们能够高效地解决这个问题。算法结合了 Tarjan\texttt{Tarjan}Tarjan 算法找桥和 BFS\texttt{BFS}BFS 找最小环的技巧,在合理的时间复杂度内解决了问题。

对于类似的图论问题,这种"先分解再求解"的思路往往能够简化问题,提高解题效率。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值