欢迎访问我的Uva题解目录 https://blog.csdn.net/richenyunqi/article/details/81149109
题目描述
题意解析
平面上有n个点,你的任务是让所有n个点连通。为此,你可以新建一些边,费用等于两个端点的欧几里德距离。另外还有q个“套餐”可以购买,如果你购买了第个套餐,该套餐中的所有结点将变得相互连通。第个套餐的花费为。
算法设计
参考《算法竞赛入门经典(第2版)》中的提示:
最容易想到的算法是:先枚举购买哪些套餐,把套餐中包含的边的权值设为0,然后求最小生成树。由于枚举量为,给边排序的时间复杂度为,而排序之后每次Kruskal算法的时间复杂度为,因此总时间复杂度为,对于题目的规模来说太大了。
只需一个小小的优化即可降低时间复杂度:先求一次原图(不购买任何套餐)的最小生成树,得到条边,然后每次枚举完套餐后只考虑套餐中的边和这条边,则枚举套餐之后再求最小生成树时,图上的边已经寥寥无几。
为什么可以这样呢?首先回顾一下,在Kruskal算法中,哪些边不会进入最小生成树。答案是:两端已经属于同一个连通分量的边。买了套餐以后,相当于一些边的权变为0,而对于不在套餐中的每条边e,排序在e之前的边一个都没少,反而可能多了一些权值为0的边,所以在原图Kruskal时被“扔掉”的边,在后面的Kruskal中也一样会被扔掉。
本题还有一个地方需要说明:因为Kruskal在连通分量包含n个点时会终止,所以对于随机数据,即使用原始的“暴力算法”,也能很快出解。如果你是命题者,可以这样出一个数据:有一个点很远,而其他n-1个点相互比较近。这样,相距较近的n-1个点之间的条边会排序在前面,每次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;
}