【例 2】北极通讯网络(信息学奥赛一本通- P1487)

【题目描述】

原题来自:Waterloo University 2002

北极的某区域共有 n 座村庄,每座村庄的坐标用一对整数 (x,y) 表示。为了加强联系,决定在村庄之间建立通讯网络。通讯工具可以是无线电收发机,也可以是卫星设备。所有的村庄都可以拥有一部无线电收发机, 且所有的无线电收发机型号相同。但卫星设备数量有限,只能给一部分村庄配备卫星设备。

不同型号的无线电收发机有一个不同的参数 d,两座村庄之间的距离如果不超过 d 就可以用该型号的无线电收发机直接通讯,d 值越大的型号价格越贵。拥有卫星设备的两座村庄无论相距多远都可以直接通讯。

现在有 k 台卫星设备,请你编一个程序,计算出应该如何分配这 k 台卫星设备,才能使所拥有的无线电收发机的 d 值最小,并保证每两座村庄之间都可以直接或间接地通讯。

例如,对于下面三座村庄:

其中 |AB|=10,|BC|=20,|AC|=10√5≈22.36

如果没有任何卫星设备或只有 1 台卫星设备 (k=0 或 k=1),则满足条件的最小的 d=20,因为 A 和 B,B 和 C 可以用无线电直接通讯;而 A 和 C 可以用 B 中转实现间接通讯 (即消息从 A 传到 B,再从 B 传到 C);

如果有 2 台卫星设备 (k=2),则可以把这两台设备分别分配给 B 和 C ,这样最小的 d 可取 10,因为 A 和 B 之间可以用无线电直接通讯;B 和 C 之间可以用卫星直接通讯;A 和 C 可以用 B 中转实现间接通讯。

如果有 3 台卫星设备,则 A,B,C 两两之间都可以直接用卫星通讯,最小的 d 可取 0。

【输入】

第一行为由空格隔开的两个整数 n,k;

第 2∼n+1 行,每行两个整数,第 i 行的 xi,yi​ 表示第 i 座村庄的坐标 (xi,yi)。

【输出】

一个实数,表示最小的 d 值,结果保留 2 位小数。

【输入样例】

3 2 10 10 10 0 30 0

【输出样例】

10.00

【提示】

数据范围:

对于全部数据,1≤n≤500,0≤x,y≤10^4,0≤k≤100。

1. 题意转换

用一句话剥离题目的“北极、村庄、卫星”的包装故事:

给定平面上的N个点,求将其划分为k个连通块(即构建一棵包含k棵树的最小生成森林)时,各连通块内部所有边的最大值中的最小值(最小瓶颈边)。

2. 思考过程与解题思路

面对这道题,第一直觉是什么?

很多同学看到“通讯网络”、“连通”、“最大距离最小”,直觉可能是二分答案:二分出一个距离d,然后用 BFS/DFS 检查在这个距离下,图是否能形成个连通块。这种做法时间复杂度约为 O(N^2 log(值域)),虽然能过,但在处理浮点数二分时极易遭遇精度死循环(TLE或WA)。

考点锁定:

  • 关键字:“保证直接或间接通讯”、“使最大的d最小”。这简直是把kruskal算法的“最小瓶颈生成树”特性贴在了脸上。

  • Kruskal 算法不仅能求出边权总和最小的生成树,它找出来的树还自带神级属性:它包含的最大那条边,在所有可能让图连通的方案中,绝对是最小的!

如何一步步推导到正解?

  1. 没有卫星时(k=0或1):我们需要用无线电把N个村庄连成1棵完整的大树,此时恰好需要连接N-1条边。

  2. 有k台卫星时():卫星堪称“神级网线”,无论多远都能瞬间连通。如果我们把这k台卫星分别发给k个互相独立的连通分量,卫星就能在云端把它们免费缝合起来。

  3. 核心数学映射:我们的无线电不再需要把图连成 1 棵树,只需要把N个孤立的村庄连成k个连通分量(也就是一片包含k棵树的森林)。在图论中,从N个点合并且只剩k个连通块,恰好需要成功连接N-k条边。

  4. 得出结论:因为 Kruskal 是按边长从小到大贪心加边的。当我们成功加上第N-k条边时,这片森林刚刚好建成,而这最后加进去的第N-k条边,必然是网络中最长的一条边,它的长度就是题目要求的最小的d。

3. 算法设计与样例推演

核心逻辑与数据结构:

  • 坐标与距离计算:用结构体存点,双重循环计算任意两点间的欧几里得距离,构建完全图(跳过重复边)。

  • 边集数组edge:存储两点及距离平方(避免浮点精度问题)。

  • 并查集(DSU):用于 Kruskal 贪心加边时的判环和合并。

极简数据手玩推演:

带入题目样例:N=3 座村庄,k=2 台卫星。

  1. 目标边数:N-k = 3-2 = 1条边。

  2. 建边与排序:AB距离为 10,BC为 20,AC约为 22.36。排序后:AB(10)BC(20)AC(22.36)。

  3. Kruskal 连线:

    • 取出最短边 AB,A 和 B 不在同一连通块,合并它们。此时成功连接了 1 条边。

    • 判定结束:已达到目标边数 1!最后加进去的边长是 10,直接输出 10.00。与样例完全吻合!

4. 时空复杂度分析

  • 时间复杂度:。,建完全图会产生约条边,即。Kruskal 的瓶颈在sort,复杂度为,约执行次运算,C++ 在 1 秒内极其轻松过关。

  • 空间复杂度:。需要一个长度为M的边集数组。即便开到 500010,也仅占用十几 MB 的内存,远低于题目要求的 64MB 限制。

5. 坑点与易错总结

  1. 浮点数排序丢失精度(极易 WA):如果在结构体里直接存sqrt开方后的double距离,在sort比较时极易遇到浮点数尾数精度丢失导致的排序错乱。(本题代码完美避开了这一点,后文详述)

  2. 特判遗漏(极易 RE/WA):如果卫星比村庄还多(),或者只有一个村庄(N=1),此时不需要连任何边,应当直接输出0.00并结束程序。

  3. 无向图重复建边拖慢常数:建图时如果写i!=j会把边存两遍,导致sort的常数翻倍。必须用j>i来只存右上三角矩阵。

6. 代码进阶与优化剖析(核心教学点)

这篇题解的代码是一份几近完美的标程,其中展现了两个极高阶的竞赛编码习惯,强烈建议所有备考 CSP 的同学刻在 DNA 里:

  • 高阶技巧 1:延迟开方(降维打击精度问题)

    仔细看代码:long long val; //存边长的平方 避免开方过程中产生精度问题。

    作者极其聪明地在sort时比较的是距离的平方(绝对精度的整数),完全避免了引入double。直到程序最后一行输出答案时,才对其使用一次sqrt。这不仅避免了任何精度被卡的可能性,而且规避了数十万次sqrt函数的高昂调用开销,让程序飞快!

  • 高阶技巧 2:极其稳健的 DSU 合并

    作者的uni函数写的是int faa = find(a);而不是随意的fa[a]。这保证了无论在什么时机调用合并,拿到的绝对是真正的“树根”,彻底断绝了并查集结构崩塌的隐患。

针对本代码的微小优化空间(代码洁癖层面):

目前代码在 Kruskal 的for循环内部,每次加边都要进行一次if(k>1)的条件判断。实际上,“目标边数”在读入N和k后就已经固定了。我们可以将n-k或n-1的计算提取到循环外部,使得核心循环的代码更加清爽。在下方的批注中已标出该优化思路。

7. 完整代码

(代码完全保留我的实现,请注意其中优化批注)

//我们思考一下k==0的情况和k==1的情况 那就是卫星设备完全不起任何作用 //因为我们要保证每两座村庄之间都可以直接或间接地通讯 且要让d最小 //那么我们就要尽可能让最长的边尽可能短 //然后我们就想到kruskal的原理了 把所有边按照从小到大排序 //然后每次选出最短的边并检查该边的两个点是否已经属于同一个连通分量 //如果不属于 就连通 否则就继续判断下一条边 //所以我们用kruskal找出来的树 //不仅总花费最小 而且它包含的最大那条边 在所有可能的生成树中绝对是最小的 //那么当k大于等于2的时候 这k台卫星可以无视距离 //把k个互相独立的连通分量连接在一起 //所以我们的无线电不需要把所有村庄连成1棵大树(不需要加n-1条边) //无线电只需要负责把n个孤立的村庄 //连成恰好k个连通分量(即一棵包含k棵树的最小生成森林) //重点来了:在图论中从n个孤立的点 合并成k个连通分量 恰好需连接n-k条边 //因为kruskal是按边长从小到大贪心加边的 //所以 当我们成功加上第n-k条无线电边时 这片森林就刚好建成了 //而刚刚加进去的这第n-k条边 必然是整个无线电网络中最长的一条边 //它的长度 就是题目要求我们找的最小的d值 #include <iostream> #include <cmath> #include <algorithm> #include <iomanip>//对应setpresicion using namespace std; int n,k;//n座村庄 k台卫星设备 int fa[510];//fa[i]为i点集合的老大 //存每个点的坐标 struct node{ int x; int y; }no[510]; //边集数组 struct edge{ int u; int v; long long val;//存边长的平方 避免开方过程中产生精度问题 //按照边长从小到大排序 friend bool operator <(edge a,edge b){ return a.val<b.val; } }e[500010]; int idx;//存进边集数组的边数 //并查集查询并进行路径压缩 int find(int x){ if(fa[x]==x) return x; return fa[x]=find(fa[x]); } //并查集合并 void uni(int a,int b){ int faa=find(a); int fab=find(b); if(faa!=fab) fa[faa]=fab; } int main(){ ios::sync_with_stdio(false); cin.tie(0); cin>>n>>k; //存每个点的坐标 for(int i=1;i<=n;i++) cin>>no[i].x>>no[i].y; //遍历每个点i与点j之间的边 for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ //省去所有重复的边 节省sort的时间 if(j>i){ idx++; e[idx].u=i; e[idx].v=j; e[idx].val=(no[i].x-no[j].x)*(no[i].x-no[j].x)+(no[i].y-no[j].y)*(no[i].y-no[j].y); } } } //把边按边长从小到大排序 sort(e+1,e+idx+1); //初始化所有点起始时集合的老大是自己 for(int i=1;i<=n;i++) fa[i]=i; int idx2=0;//idx2记录连了多少条边 //如果卫星设备数量比村庄总数多或总共只有一个村庄 输出0 直接结束程序 if(k>=n||n==1){ cout<<fixed<<setprecision(2)<<0.00; return 0; } //【优化批注】:此处可以提前计算目标边数,消去循环内的分支判定。 // 例如:int target_edges = (k <= 1) ? (n - 1) : (n - k); //接下来遍历所有边 for(int i=1;i<=idx;i++){ int a=e[i].u; int b=e[i].v; //如果a点和b点不属于一个连通分量 就连接它们 if(find(a)!=find(b)){ uni(a,b); idx2++;//连接的边数加1 } //如果k>1只需要连n-k条边即可 if(k>1){ if(idx2==n-k){ cout<<fixed<<setprecision(2)<<sqrt(e[i].val); return 0; } } //如果k==0或k==1都需要连n-1条边 即需要无线电连接所有村庄 else{ if(idx2==n-1){ cout<<fixed<<setprecision(2)<<sqrt(e[i].val); return 0; } } //【优化批注】:如果使用了 target_edges,上述逻辑可简化为: //if(idx2==target_edges) //{cout<<fixed<<setprecision(2)<<sqrt(e[i].val); return 0;} } return 0; }

相关新闻

STM32F411标准例程模板:从工程搭建到外设驱动开发实战

STM32F411标准例程模板:从工程搭建到外设驱动开发实战

简介&#xff1a;面向 STM32F411 开发者的一份标准例程模板&#xff0c;基于 ARM Cortex-M4 内核&#xff0c;涵盖启动代码、HAL 库、系统时钟配置、主函数框架及 LED/串口/定时器等外设示例&#xff0c;适合快速搭建项目并学习片上外设用法。压缩包 5.47MB&#xff0c;共 212 …

2026/10/21 6:07:27 阅读更多 →
HTTP实战笔记:从协议底层逻辑到线上问题排查的工程指南

HTTP实战笔记:从协议底层逻辑到线上问题排查的工程指南

1. 为什么值得重新聊一聊 HTTP做后端开发或者前端联调时间长了&#xff0c;你会发现一个现象&#xff1a;很多人对 HTTP 的理解停留在“请求-响应”“GET 和 POST 的区别”这个层面&#xff0c;一旦遇到线上问题——比如接口偶发超时、浏览器缓存不生效、跨域预检请求莫名多了一…

2026/10/21 6:06:26 阅读更多 →
C#刷LeetCode完整方法论:从看题就懵到每天AC的实操指南

C#刷LeetCode完整方法论:从看题就懵到每天AC的实操指南

从“看题就懵”到“每天AC”——我用C#刷LeetCode的完整方法论如果你是一个C#开发者&#xff0c;想靠LeetCode提升算法能力&#xff0c;但打开题库就不知道从哪下手&#xff0c;或者刷了几天觉得“C#写算法怎么这么别扭”&#xff0c;那这篇文章就是写给你看的。我本人是.NET方…

2026/10/21 6:06:26 阅读更多 →

最新新闻

Linux僵尸、孤儿与守护进程:原理、区别与实战实现

Linux僵尸、孤儿与守护进程:原理、区别与实战实现

1. 项目概述&#xff1a;从“进程尸体”到“后台幽灵”的真实世界映射在 Linux 系统里&#xff0c;你执行ps aux | grep defunct时看到的<defunct>标记&#xff0c;不是报错&#xff0c;而是一具“进程尸体”——它已经死亡&#xff0c;却还没被收尸&#xff1b;你用nohu…

2026/10/21 6:57:46 阅读更多 →
Docker部署Redis的四大核心问题:网络、持久化、配置、集群宣告

Docker部署Redis的四大核心问题:网络、持久化、配置、集群宣告

1. 为什么非得用 Docker 部署 Redis&#xff1f;——从三台裸机故障说起去年在某高校实验室做模拟项目X的后端支撑时&#xff0c;我负责缓存层搭建。最初按老办法&#xff1a;三台物理服务器&#xff0c;每台手动编译安装 Redis 6.2&#xff0c;配好 bind、port、requirepass、…

2026/10/21 6:57:46 阅读更多 →
Docker精讲:从Flask项目构建生产级容器镜像

Docker精讲:从Flask项目构建生产级容器镜像

1. 为什么“快速入门”反而最容易卡在第一步&#xff1f; 很多人点开“Docker 快速入门”教程&#xff0c;前五分钟信心满满——装完 Docker Desktop、跑通 docker run hello-world &#xff0c;就以为自己已经“会了”。结果第二天想把本地写好的 Python 脚本打包成镜像&am…

2026/10/21 6:57:46 阅读更多 →
机器学习到强化学习:四大AI方向实战导航

机器学习到强化学习:四大AI方向实战导航

如果你已经跟着这个系列走到第三篇&#xff0c;大概率不是刚装上 Python 的新手了。前面两篇我们聊了环境搭建、NumPy/Pandas 手感、Scikit-learn 的套路&#xff0c;也跑通了最简单的回归和分类。这些属于"会走"的阶段。这篇导航想做的是另一件事&#xff1a;把机器…

2026/10/21 6:57:46 阅读更多 →
技能提升的底层逻辑:从硬技能到元能力的系统方法

技能提升的底层逻辑:从硬技能到元能力的系统方法

1. 从“skills”这个标题说起&#xff1a;一个被严重低估的元能力“skills”这个词&#xff0c;单独拎出来看&#xff0c;信息量几乎为零。它不像“如何用Python做数据清洗”那样具体&#xff0c;也不像“三小时搭建个人博客”那样有明确交付物。但恰恰是这种模糊性&#xff0c…

2026/10/21 6:57:46 阅读更多 →
降AI率工具怎么选?八类平台实测对比与避坑指南

降AI率工具怎么选?八类平台实测对比与避坑指南

这个选题最近被问得太多了。我自己的后台留言里&#xff0c;隔三差五就有人发来一张检测报告截图&#xff0c;问为什么文章明明是自己写的还是被标成“疑似AIGC生成”&#xff0c;或者干脆是——交作业前一天才开始慌&#xff0c;到处找降AI率的工具。说实话&#xff0c;这事在…

2026/10/21 6:56:45 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事&#xff1a;AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里&#xff0c;按宣传口径&#xff0c;本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”&#xff0c;而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起&#xff1a;用户到底在追求什么“封号重开”这四个字&#xff0c;放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流&#xff0c;背后是大量的时间和精力成本。正常情况下&#xff0c;没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”&#xff0c;而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时&#xff0c;就意识到&#xff1a;单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/21 4:27:17 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →