P2323 [HNOI2006] 公路修建问题

解题思路

这道题目是一个典型的最小生成树问题,但有两个特殊要求:

  1. 必须选择至少k条一级公路

  2. 需要使所有公路中花费最大的那条的花费尽可能小

方法思路

  1. 双阶段Kruskal算法:我们分两个阶段来构建生成树

    • 第一阶段:只考虑一级公路,构建部分生成树,确保至少有k条一级公路

    • 第二阶段:考虑所有公路(优先选择二级公路),完成剩余的连接

  2. 贪心选择:在每个阶段都按照边的权值从小到大排序,确保每次选择的都是当前最小的边

  3. 并查集管理连通性:使用并查集来高效管理节点的连通性,避免形成环

代码注释

#include<bits/stdc++.h>
#define pii pair<int,int>
using namespace std;
const int N = 2e5 + 10;

// 定义边的结构体,包含两个端点、一级/二级公路花费、是否被选中标记、输入时的编号
struct node{
    int x,y,z1,z2,f,id; 
    // x,y: 边的两个端点
    // z1: 一级公路花费
    // z2: 二级公路花费
    // f: 是否被选中(0/1)
    // id: 边的原始输入编号
};
node t[N]; // 存储所有边的数组
int n,m,k; // n:景点数, m:可修公路数, k:需要的一级公路最小数量
int ans,sum; // ans:最大花费, sum:已选边数
pii a[N]; // 存储结果边的数组(first:边编号, second:公路等级)
int top; // 结果边的数量
int f[N]; // 并查集父节点数组

// 比较函数:按一级公路花费升序排序
bool cmp1(node a,node b)
{
    return a.z1 < b.z1;
}

// 比较函数:按二级公路花费升序排序
bool cmp2(node a,node b)
{
    return a.z2 < b.z2;
}

// 比较函数:按边的原始编号升序排序(用于结果输出)
bool cmp3(pii a,pii b)
{
    return a.first < b.first;
}

// 并查集查找函数
int find(int x)
{
    if(f[x] != x) f[x] = find(f[x]);
    return f[x];
}

// 并查集合并函数
void merge(int x,int y)
{
    int fx = find(x),fy = find(y);
    f[fy] = fx;
}

// Kruskal算法实现
// cnt: 需要选择的边数
// op: 阶段标识(1:第一阶段只选一级公路, 2:第二阶段可选任意公路)
void kruskal(int cnt,int op)
{
    for(int i = 1; i <= m - 1; i++)
    {
        int x = t[i].x,y = t[i].y;
        // 如果两个端点不连通且该边未被选中
        if(find(x) != find(y) && t[i].f == 0)
        {
            merge(x,y); // 合并两个集合
            t[i].f = 1; // 标记边为已选
            sum++; // 已选边数+1
            
            // 第一阶段处理
            if(op == 1) {
                ans = max(ans,t[i].z1); // 更新最大花费
                a[++top] = {t[i].id,op}; // 记录结果(强制一级公路)
            }
            
            // 第二阶段处理
            if(op == 2)
            {
                // 选择花费较小的公路类型
                ans = max(ans,min(t[i].z1,t[i].z2)); // 更新最大花费
                if(t[i].z1 <= t[i].z2) 
                    a[++top] = {t[i].id,1}; // 选一级公路
                else 
                    a[++top] = {t[i].id,2}; // 选二级公路
            }
            
            if(sum == cnt) break; // 已选够所需边数,退出
        }
    }
}

int main()
{
    cin >> n >> k >> m;
    // 读入所有边信息
    for(int i = 1; i <= m - 1; i++) 
    {
        int x,y,z1,z2; cin >> x >> y >> z1 >> z2;
        t[i] = {x,y,z1,z2,0,i}; // 初始化边信息
    }
    
    // 初始化并查集
    for(int i = 1; i <= n; i++) f[i] = i;
    
    // 第一阶段:优先选择一级公路,确保至少有k条
    sort(t + 1,t + 1 + m - 1,cmp1); // 按一级公路花费排序
    kruskal(k,1); // 选择k条一级公路
    
    // 第二阶段:选择剩余需要的边,优先选择二级公路
    sort(t + 1,t + 1 + m - 1,cmp2); // 按二级公路花费排序
    kruskal(n - 1,2); // 选择剩余n-1-k条边
    
    // 输出结果
    cout << ans << endl; // 输出最大花费
    sort(a + 1,a + 1 + top,cmp3); // 按原始边编号排序结果
    for(int i = 1; i <= top; i++)
        cout << a[i].first << " " << a[i].second << endl; // 输出每条边的编号和等级
    
    return 0;
}

复杂度分析

  1. 时间复杂度:O(m log m),主要来自两次排序操作

  2. 空间复杂度:O(n + m),用于存储边和并查集结构

该算法通过两次Kruskal算法,先确保满足一级公路数量要求,再以最小花费完成整个生成树的构建,保证了最优解。

posted @ 2025-05-27 20:14  CRt0729  阅读(16)  评论(0)    收藏  举报