欢迎访问我的Uva题解目录 https://blog.csdn.net/richenyunqi/article/details/81149109

题目描述

例题11-3 买还是建(Buy or Build, ACM/ICPC SWERC 2005, UVa1151)

题意解析

平面上有n个点1n1000(1≤n≤1000),你的任务是让所有n个点连通。为此,你可以新建一些边,费用等于两个端点的欧几里德距离。另外还有q0q8(0≤q≤8)个“套餐”可以购买,如果你购买了第ii个套餐,该套餐中的所有结点将变得相互连通。第ii个套餐的花费为CiC_i

算法设计

参考《算法竞赛入门经典(第2版)》中的提示:

最容易想到的算法是:先枚举购买哪些套餐,把套餐中包含的边的权值设为0,然后求最小生成树。由于枚举量为O(2q)O(2^q),给边排序的时间复杂度为O(n2logn)O(n^2logn),而排序之后每次Kruskal算法的时间复杂度为O(n2)O(n^2),因此总时间复杂度为O(2qn2+n2logn)O(2^qn^2+n^2logn),对于题目的规模来说太大了。
只需一个小小的优化即可降低时间复杂度:先求一次原图(不购买任何套餐)的最小生成树,得到n1n-1条边,然后每次枚举完套餐后只考虑套餐中的边和这n1n-1条边,则枚举套餐之后再求最小生成树时,图上的边已经寥寥无几。
为什么可以这样呢?首先回顾一下,在Kruskal算法中,哪些边不会进入最小生成树。答案是:两端已经属于同一个连通分量的边。买了套餐以后,相当于一些边的权变为0,而对于不在套餐中的每条边e,排序在e之前的边一个都没少,反而可能多了一些权值为0的边,所以在原图Kruskal时被“扔掉”的边,在后面的Kruskal中也一样会被扔掉。
本题还有一个地方需要说明:因为Kruskal在连通分量包含n个点时会终止,所以对于随机数据,即使用原始的“暴力算法”,也能很快出解。如果你是命题者,可以这样出一个数据:有一个点很远,而其他n-1个点相互比较近。这样,相距较近的n-1个点之间的Cn12C_{n-1}^2条边会排序在前面,每次Kruskal都会先考虑完所有这些边。而考虑这些边时是无法让远点和近点连通的。

总结起来就是,先对原图进行一次Kruskal算法,得到最小生成树的权值,并将最小生成树的所有边存储起来。接着枚举购买的套餐,将购买的套餐中的点视为相互连通,并求出所有套餐费用之和allCost,遍历刚刚存储好的原图的最小生成树的边,将连接未连通的两个结点的边的权值加到allCost中。所有枚举得到的allCost与最小生成树权值中的最小值即为所求。

C++代码

#include<bits/stdc++.h>
using namespace std;
struct Edge{
    int v1,v2,cost;
    Edge(int vv1,int vv2,int c):v1(vv1),v2(vv2),cost(c) {}
    bool operator <(const Edge&e)const{
        return this->cost>e.cost;
    }
};
priority_queue<Edge>allEdges;//存储图中的所有边
vector<Edge>treeEdges;//存储最小生成树的所有边
vector<pair<int,vector<int>>>subnetworks;//存储所有套餐
vector<int>sub;//存储购买的套餐编号,用于进行枚举
int father[1005],t,n,q,m,a,b,c;//并查集
int findFather(int x){//寻找根结点并进行路径压缩
    if(x==father[x])
        return x;
    int t=findFather(father[x]);
    father[x]=t;
    return t;
}
void unionSets(Edge e,int&cost,int flag){//合并边两端的结点为同一集合。
    int ua=findFather(e.v1),ub=findFather(e.v2);
    if(ua!=ub){
        cost+=e.cost;//将边的权值加到cost上
        father[ua]=ub;//合并两个集合
        if(flag==0)//flag==0表示求解最小生成树
            treeEdges.push_back(e);
    }
}
void DFS(int i,int&ans){//枚举购买的套餐
    if(i==subnetworks.size()){//套餐枚举完成
        int allCost=0;//存储购买当前套餐后的总费用
        iota(father,father+n+1,0);//初始化并查集
        for(auto&i:sub){//遍历购买的套餐编号
            allCost+=subnetworks[i].first;//加上当前套餐的费用
            vector<int>&t=subnetworks[i].second;//当前套餐包含的结点
            for(int j=0;j<t.size();++j)//将套餐内的结点合并到一个集合
                for(int k=j+1;k<t.size();++k)
                    unionSets(Edge(t[j],t[k],0),allCost,1);
        }
        for(Edge&e:treeEdges)//遍历最小生成树中的边
            unionSets(e,allCost,1);//合并边两端的结点
        ans=min(ans,allCost);//更新ans
        return;
    }
    sub.push_back(i);//购买该套餐
    DFS(i+1,ans);
    sub.pop_back();//不购买该套餐
    DFS(i+1,ans);
}
int main(){
    scanf("%d",&t);
    for(int ii=0;ii<t;++ii){
        printf("%s",ii>0?"\n":"");
        treeEdges.clear();
        subnetworks.clear();
        vector<pair<int,int>>cities={{0,0}};//存储结点的坐标
        scanf("%d%d",&n,&q);
        while(q--){
            scanf("%d%d",&m,&c);
            subnetworks.push_back({c,{}});
            while(m--){
                scanf("%d",&a);
                subnetworks.back().second.push_back(a);
            }
        }
        for(int i=1;i<=n;++i){
            scanf("%d%d",&a,&b);
            for(int j=1;j<cities.size();++j)//求所有的边
                allEdges.push(Edge(j,i,(a-cities[j].first)*(a-cities[j].first)+(b-cities[j].second)*(b-cities[j].second)));
            cities.push_back({a,b});
        }
        int ans=0;
        iota(father,father+n+1,0);//初始化并查集
        while(!allEdges.empty()){//求解最小生成树
            Edge e=allEdges.top();
            allEdges.pop();
            unionSets(e,ans,0);
        }
        DFS(0,ans);
        printf("%d\n",ans);
    }
    return 0;
}

相关文章: