POJ2728 Desert King(沙漠之王)完整题解|01分数规划 + Prim最小生成树
POJ2728 Desert King(沙漠之王)完整题解|01分数规划 + Prim最小生成树
一、题目概述

题意
给定 $n$ 个村庄,每个村庄拥有平面坐标 $(x,y)$ 和海拔高度 $z$。任意两个村庄之间可以修建渠道:
- 渠道长度:两点平面欧几里得距离 $b_{i,j}=\sqrt{(x_i-x_j)2+(y_i-y_j)2}$;
- 渠道成本:两点海拔高度差 $a_{i,j}=|z_i-z_j|$。
我们需要修建渠道连通全部村庄,构成一棵生成树,要求最小化比值:
$$R=\frac{\text{所有渠道总成本之和}}{\text{所有渠道总长度之和}}$$
最终输出最小比值,保留三位小数并四舍五入。
输入输出说明
- 多组测试用例,每组第一行输入整数 $n$,当 $n=0$ 时终止程序;
- 后续 $n$ 行每行给出 $x,y,z$,依次代表每个村庄的坐标与海拔;
- 每组数据单独输出一行浮点数,小数点后保留三位。
样例简要说明
样例共4个村庄,最优生成树选取3条相邻边,总成本和为3,总长度和为3,计算得到比值 $1.000$,与样例输出完全吻合。

二、算法核心思路:01分数规划
1. 数学变形推导
设最优比值为 $R$,我们二分一个猜测值 $mid$,判断是否存在一棵生成树满足:
$$\frac{\sum a_e}{\sum b_e} \le mid$$
式中 $e$ 代表生成树包含的边。
由于任意渠道长度 $b_e>0$,不等式两边同乘总长度,不等号方向不变:
$$\sum a_e - mid \cdot \sum b_e \le 0 \implies \sum \big(a_e - mid \cdot b_e\big) \le 0$$
2. 判定逻辑
对原图每一条边构造新权值 $w_{i,j}=a_{i,j}-mid \cdot b_{i,j}$,问题转化为:
求整张图的最小生成树,统计最小生成树所有边的权值总和:
- 若最小生成树总权和 $\le 0$:存在合法方案,当前猜测值 $mid$ 偏大,我们可以尝试更小的比值;
- 若最小生成树总权和 $>0$:所有生成树的比值都大于 $mid$,需要放大猜测的比值。
3. 二分单调性
该比值满足单调性质:若 $mid$ 是可行解,则所有大于 $mid$ 的数值都可行;若 $mid$ 不可行,则所有小于 $mid$ 的数值都不可行。依靠单调性,使用浮点二分不断缩小区间,逼近最优实数解。
三、最小生成树选择:Prim算法
题目 $n\le1000$,任意两点之间都存在边,属于稠密完全图:
- Prim 算法时间复杂度 $O(n^2)$,稠密图下效率远优于 Kruskal;
- 算法流程:
- 定义数组 $d[]$,$d[i]$ 表示当前已选点集合到点 $i$ 的最小边权;
- 初始化起点 $d[1]=0$,其余点初始化为无穷大;
- 循环 $n$ 次,每次选出未访问、$d$ 值最小的点加入生成树;
- 用新加入的点松弛更新其余未访问点的最小边权;
- 累加最小生成树总权值,返回判定结果。
四、完整AC代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
const double INF = 1e18;
int n, x[N], y[N], z[N], vis[N];
double a[N][N], b[N][N], d[N];
// 计算两点之间渠道成本(海拔高度差)
int geta(int i, int j){
return abs(z[i] - z[j]);
}
// 计算两点之间渠道水平长度(平面欧几里得距离)
double getb(int i, int j){
long long dx = x[i] - x[j];
long long dy = y[i] - y[j];
return sqrt(1.0 * dx * dx + 1.0 * dy * dy);
}
// 二分判定函数:给定猜测比值mid,求最小生成树总权和,返回是否可行
bool prim(double mid){
memset(vis, 0, sizeof vis);
// 初始化距离数组为无穷大
for(int i = 1; i <= n; i++) d[i] = INF;
d[1] = 0;
double sum_mst = 0;
for(int i = 1; i <= n; i++){
// 选出未访问且d值最小的点t
int t = -1;
for(int j = 1; j <= n; j++){
if(!vis[j]){
if(t == -1 || d[j] < d[t]){
t = j;
}
}
}
sum_mst += d[t];
vis[t] = 1;
// 松弛更新所有未访问点的最小边权
for(int j = 1; j <= n; j++){
if(!vis[j]){
double w = a[t][j] - mid * b[t][j];
if(w < d[j]){
d[j] = w;
}
}
}
}
// 最小生成树总权和<=0,说明mid可行
return sum_mst <= 0;
}
// 浮点二分主函数,逼近最优比值
double binary_find(){
double l = 0.0, r = 1e7;
// 精度1e-6,保证输出三位小数无误差
while(r - l > 1e-6){
double mid = (l + r) / 2;
if(prim(mid)){
r = mid;
}else{
l = mid;
}
}
return r;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
// 多组输入,n=0时结束循环
while(cin >> n && n != 0){
// 读取所有村庄坐标、海拔
for(int i = 1; i <= n; i++){
cin >> x[i] >> y[i] >> z[i];
}
// 预处理所有点对的成本a、长度b
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
a[i][j] = geta(i, j);
b[i][j] = getb(i, j);
}
}
double ans = binary_find();
// 保留三位小数输出
printf("%.3lf\n", ans);
}
return 0;
}
五、代码逐段详细讲解
1. 常量与全局数组定义
const int N = 1005;
const double INF = 1e18;
int n, x[N], y[N], z[N], vis[N];
double a[N][N], b[N][N], d[N];
- $N=1005$:适配题目村庄数量上限 $n\le1000$;
- $INF=1e18$:代表无穷大,用于Prim初始化距离数组;
- $x[],y[],z[]$:存储每个村庄的坐标、海拔;
- $vis[]$:Prim算法标记点是否已加入生成树;
- $a[i][j]$:预存 $i,j$ 两点渠道成本;$b[i][j]$:预存两点渠道长度;
- $d[]$:Prim专用距离数组,记录集合到各点最小边权。
2. 辅助计算函数 geta / getb
int geta(int i, int j){
return abs(z[i] - z[j]);
}
double getb(int i, int j){
long long dx = x[i] - x[j];
long long dy = y[i] - y[j];
return sqrt(1.0 * dx * dx + 1.0 * dy * dy);
}
geta:计算两点海拔高度差,即渠道建设成本;getb:计算平面欧几里得距离,使用long long存储坐标差值,避免平方运算int溢出,最后开平方得到渠道长度。
3. prim 二分判定函数
bool prim(double mid){
memset(vis, 0, sizeof vis);
for(int i = 1; i <= n; i++) d[i] = INF;
d[1] = 0;
double sum_mst = 0;
for(int i = 1; i <= n; i++){
int t = -1;
for(int j = 1; j <= n; j++){
if(!vis[j]){
if(t == -1 || d[j] < d[t]){
t = j;
}
}
}
sum_mst += d[t];
vis[t] = 1;
for(int j = 1; j <= n; j++){
if(!vis[j]){
double w = a[t][j] - mid * b[t][j];
if(w < d[j]){
d[j] = w;
}
}
}
}
return sum_mst <= 0;
}
- 初始化
vis全为未访问,d数组设为无穷大,起点 $d[1]=0$; - 循环 $n$ 轮构建最小生成树:
- 遍历所有村庄,选出未访问、距离值最小的点 $t$;
- 将该点对应边权累加至MST总权和,标记为已访问;
- 以点 $t$ 为中介,更新其余未访问点的最小边权,边权使用分数规划构造的 $w=a-mid\cdot b$;
- 循环结束后判断最小生成树总权和是否小于等于0,返回布尔值供二分使用。
4. binary_find 浮点二分函数
double binary_find(){
double l = 0.0, r = 1e7;
while(r - l > 1e-6){
double mid = (l + r) / 2;
if(prim(mid)){
r = mid;
}else{
l = mid;
}
}
return r;
}
- 二分区间 $[0,10^7]$:高度最大 $10^7$,长度最小趋近于0,比值不会超过 $10^7$;
- 循环终止条件
r-l>1e-6:预留足够精度,保证最终保留三位小数时四舍五入准确; - 取区间中点
mid调用判定函数:prim(mid)=true:当前比值可行,收缩右边界,寻找更小比值;prim(mid)=false:当前比值过小,收缩左边界,放大猜测值;
- 循环结束后返回最优浮点解。
5. main 主函数输入输出逻辑
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
while(cin >> n && n != 0){
for(int i = 1; i <= n; i++){
cin >> x[i] >> y[i] >> z[i];
}
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
a[i][j] = geta(i, j);
b[i][j] = getb(i, j);
}
}
double ans = binary_find();
printf("%.3lf\n", ans);
}
return 0;
}
ios::sync_with_stdio(false); cin.tie(0);加速cin读取,大数据多组输入不超时;while(cin >> n && n != 0)循环读取多组样例,读到 $n=0$ 直接结束程序;- 读取全部村庄数据后,双重循环预处理所有点对的成本
a与长度b,避免重复计算; - 调用二分函数得到最优比值,
printf("%.3lf\n")格式化输出,自动保留三位小数并换行。
六、算法复杂度分析
- 浮点二分:区间 $[0,10^7]$,精度 $10^{-6}$,二分循环约50次;
- 单次Prim最小生成树:$O(n^2)$,$n\le1000$,单次运算约100万次;
总复杂度:$O(50 \times n^2)$,题目数据范围下运算量极小,无超时风险。
七、题型总结
- 模型定位:01分数规划 + 稠密图最小生成树综合题,是信息学竞赛经典中档综合算法;
- 固定解题流程:
① 分式数学变形,构造新边权 $w=a-mid\cdot b$;
② 浮点二分枚举比值;
③ 使用Prim/Kruskal求最小生成树,判定总权和符号; - 适用场景拓展:所有「最大化/最小化两类权值和的比值」且带图连通约束的题目,都可以套用这套分数规划+图论模板。
浙公网安备 33010602011771号