北极通讯网络

北极通讯网络


题目描述

image-20210725142226728


核心思路

抽象一下题意,其实题目是想要求:

找到一个最小的 d d d值,使得在删除所有权值大于 d d d的边后,所形成的连通块的个数不超过 k k k

因为题目中说到“两座村庄之间的距离如果不超过 d d d,就可以用该型号的无线电收发机直接通讯”,也就是说,那些距离不超过 d d d的节点都是可以直接或者间接连通的,但是那些距离超过 d d d的节点之间就不能通过无线电进行通信了,而需要借助卫星设备。

而且,我们发现一个事实:随着 d d d值的不断递增,所形成的连通块的个数不断递减。

下面一个图示解释了这个原因:

image-20210725143740580

一提到连通块和最小生成树,我们很容易想到Kruskal算法,回顾一下这个算法的流程,其实就是:

  • 将边权从小到大排序
  • 依次从小到大扫描每一条边,设这条边的一个端点为 A A A,另一个端点为 B B B,那么也就是合并 A A A所在连通块和 B B B所在连通块

我们发现第二步本质上就是在维护连通块的个数,而且我们知道Kruskal算法每次选择的边权都是不断递增的,而且随着边权的不断递增,就会不断合并连通块,即随着边权的递增,所得到的连通块个数不断递减。而这个性质不正好与我们上面提到的题目事实是完全相同的嘛。

也就是说,题目想要求的最小的距离 d d d值,其实就是在Kruskal算法执行过程中的某个时刻,选择了某条边的权值,然后合并这条边后,使得连通块的个数是 k k k。即距离 d d d值其实就是边权。

如下图所示:

image-20210725144429219

因此,我们就直接跑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

如下图解释:

image-20210725155710521


代码

写法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;
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值