北极通讯网络
题目描述
核心思路
抽象一下题意,其实题目是想要求:
找到一个最小的 d d d值,使得在删除所有权值大于 d d d的边后,所形成的连通块的个数不超过 k k k
因为题目中说到“两座村庄之间的距离如果不超过 d d d,就可以用该型号的无线电收发机直接通讯”,也就是说,那些距离不超过 d d d的节点都是可以直接或者间接连通的,但是那些距离超过 d d d的节点之间就不能通过无线电进行通信了,而需要借助卫星设备。
而且,我们发现一个事实:随着 d d d值的不断递增,所形成的连通块的个数不断递减。
下面一个图示解释了这个原因:
一提到连通块和最小生成树,我们很容易想到Kruskal算法,回顾一下这个算法的流程,其实就是:
- 将边权从小到大排序
- 依次从小到大扫描每一条边,设这条边的一个端点为 A A A,另一个端点为 B B B,那么也就是合并 A A A所在连通块和 B B B所在连通块
我们发现第二步本质上就是在维护连通块的个数,而且我们知道Kruskal算法每次选择的边权都是不断递增的,而且随着边权的不断递增,就会不断合并连通块,即随着边权的递增,所得到的连通块个数不断递减。而这个性质不正好与我们上面提到的题目事实是完全相同的嘛。
也就是说,题目想要求的最小的距离 d d d值,其实就是在Kruskal算法执行过程中的某个时刻,选择了某条边的权值,然后合并这条边后,使得连通块的个数是 k k k。即距离 d d d值其实就是边权。
如下图所示:
因此,我们就直接跑Kruskal算法,然后挑选一条边后,就将这条边的权值赋给 d d d,当某一时刻,合并某一条边后,设这条边的权值为 w w w,使得所形成的连通块的个数 ≤ k \leq k ≤k,那么此时的这个 w w w就是 d d d值的最小值了,直接break即可,不再进行选边操作了。
当然这一题也是可以用二分的,不过需要配合并查集算法:
我们设 性质为:删除所有边权大于 d d d的边后,对所有边权 ≤ d \leq d ≤d的边进行并查集操作后,所形成的连通块的数量 ≤ k \leq k ≤k
注意这里是浮点数的二分
- 如果满足这个性质,则 r = m i d r=mid r=mid
- 如果不满足这个性质,则 l = m i d l=mid l=mid
如下图解释:
代码
写法1:直接用Kruskal算法
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
//边数M最坏情况下是 无向完全图即n(n-1)/2
const int N=510,M=N*N/2;
typedef pair<int,int>PII;
int n,m,k;
struct Edge{
int a,b;
double w;
bool operator < (const Edge& W)const{
return w<W.w;
}
}edges[M];
int p[N];
//存储每个节点的坐标
PII q[M];
//获取两个点之间的距离 把这个距离当作这两个节点之间的边权
inline double get(PII A,PII B)
{
int dx=A.first-B.first;
int dy=A.second-B.second;
return sqrt(dx*dx+dy*dy);
}
int find(int x)
{
if(x!=p[x])
p[x]=find(p[x]);
return p[x];
}
double Kruskal()
{
int cnt=n; //起初有n个独立的节点,也就是n个连通块
sort(edges,edges+m);
//注意这里初始化时是从0开始的
//因为我们用到的点是0~n-1而不是1~n
for(int i=0;i<n;i++)
p[i]=i;
double res=0;
for(int i=0;i<m;i++)
{
int a=find(edges[i].a);
int b=find(edges[i].b);
double w=edges[i].w;
if(a!=b)
{
p[a]=b;
//记录此时选的那条边的权值
res=w;
//合并两个连通块了,因此连通块的数量-1
cnt--;
}
//如果合并完这条边后,所形成的连通块的个数<=k 那么选中的这条边其实就是最小的d值
//那么就不需要再 选择后面的边了
if(cnt<=k)
break;
}
return res;
}
int main()
{
scanf("%d%d",&n,&k);
//读入这n个村庄的坐标
for(int i=0;i<n;i++) //村庄编号从0到n-1而不是从1到n
scanf("%d%d",&q[i].first,&q[i].second);
//由于是无向边,因此是对称矩阵 那么我们只需要统计下三角的边就行了
for(int i=0;i<n;i++)
{
for(int j=0;j<i;j++)
{
double w=get(q[i],q[j]);
edges[m++]={i,j,w};
}
}
double d=Kruskal();
printf("%.2lf\n",d);
return 0;
}
写法2:二分+并查集
#include<iostream>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
const int N=510,M=N*N/2;
const double eps=1e-4;
typedef pair<int,int>PII;
int n,m,k;
int p[N];
PII q[M];
struct Edge{
int a,b;
double w;
bool operator<(const Edge &W)const{
return w<W.w;
}
}edges[M];
inline double get(PII A,PII B)
{
int dx=A.first-B.first;
int dy=A.second-B.second;
return sqrt(dx*dx+dy*dy);
}
int find(int x)
{
if(x!=p[x])
p[x]=find(p[x]);
return p[x];
}
//判断mid是否满足该性质:删除所有边权大于d的边后,对所有边权<=d的边进行并查集操作后
//所形成的连通块的数量<=k
bool check(double mid)
{
//注意这里初始化时是从0开始的
//因为我们用到的点是0~n-1而不是1~n
for(int i=0;i<n;i++)
p[i]=i;
//idx记录的是第一个大于权值为mid的那条边所对应的数组下标
int idx;
for(int i=0;i<m;i++)
{
double w=edges[i].w;
//浮点数做差会有精度问题 应该需要加入精度比较
if(w>mid+eps)
{
idx=i;
break;
}
}
int res=0; //用多少条边来连接不同的连通块
//由于第idx条边是首个权值大于mid的边,因此[0,idx-1]这些边都是权值<=mid的边
//而我们现在就是要对这些权值<=mid的边进行合并 然后求出连通块的个数
for(int i=0;i<idx;i++)//注意这里是[0,idx)也就是[0,idx-1]
{
int a=find(edges[i].a);
int b=find(edges[i].b);
if(a!=b)
{
//合并两个连通块需要耗费一条边
res++;
p[a]=b;
}
}
//连通块的数目=总点数-耗费的连边数量
//比如有u,v,w三个节点,起初是独立的三个连通块,耗费一条边连接u,v,那么u和v在同一个连通块中了,num=1;
//耗费一条边连接(u,v),w,那么u,v,w在同一个连通块中了,num=2;最终只有一个连通块
//因此连通块数量cnt=总点数n-耗费的连边数量num
int cnt=n-res;
return cnt<=k;
}
int main()
{
scanf("%d%d",&n,&k);
//读入这n个村庄的坐标
for(int i=0;i<n;i++) //村庄编号从0到n-1而不是从1到n
scanf("%d%d",&q[i].first,&q[i].second);
//由于是无向边,因此是对称矩阵 那么我们只需要统计下三角的边就行了
for(int i=0;i<n;i++)
{
for(int j=0;j<i;j++)
{
double w=get(q[i],q[j]);
edges[m++]={i,j,w};
}
}
//给所有边按从小到大排序
sort(edges,edges+m);
// 二分答案 答案区间是读入的边权的最小值和边权的最大值
double l=0,r=edges[m-1].w;
//进行浮点数二分
while(l+eps<r)
{
double mid=(l+r)/2;
//check是检查当前的mid是否满足删除所有d>mid的边后,对所有<=mid的边进行合并之后所形成的连通块数目<=k
//设答案为ans,对于d>=ans的d都是满足的,如果满足,为了求出最小的d,则往左侧收缩
if(check(mid))
r=mid; //往左侧收缩
//否则不满足该性质 应该往右边去寻找
else
l=mid;
}
printf("%.2lf",l);
return 0;
}