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

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

一、题目概述

请添加图片描述

题意

给定 $n$ 个村庄,每个村庄拥有平面坐标 $(x,y)$ 和海拔高度 $z$。任意两个村庄之间可以修建渠道:

  1. 渠道长度:两点平面欧几里得距离 $b_{i,j}=\sqrt{(x_i-x_j)2+(y_i-y_j)2}$;
  2. 渠道成本:两点海拔高度差 $a_{i,j}=|z_i-z_j|$。

我们需要修建渠道连通全部村庄,构成一棵生成树,要求最小化比值:
$$R=\frac{\text{所有渠道总成本之和}}{\text{所有渠道总长度之和}}$$
最终输出最小比值,保留三位小数并四舍五入。

输入输出说明

  1. 多组测试用例,每组第一行输入整数 $n$,当 $n=0$ 时终止程序;
  2. 后续 $n$ 行每行给出 $x,y,z$,依次代表每个村庄的坐标与海拔;
  3. 每组数据单独输出一行浮点数,小数点后保留三位。

样例简要说明

样例共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$,任意两点之间都存在边,属于稠密完全图:

  1. Prim 算法时间复杂度 $O(n^2)$,稠密图下效率远优于 Kruskal;
  2. 算法流程:
    1. 定义数组 $d[]$,$d[i]$ 表示当前已选点集合到点 $i$ 的最小边权;
    2. 初始化起点 $d[1]=0$,其余点初始化为无穷大;
    3. 循环 $n$ 次,每次选出未访问、$d$ 值最小的点加入生成树;
    4. 用新加入的点松弛更新其余未访问点的最小边权;
    5. 累加最小生成树总权值,返回判定结果。

四、完整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;
}
  1. 初始化vis全为未访问,d数组设为无穷大,起点 $d[1]=0$;
  2. 循环 $n$ 轮构建最小生成树:
    • 遍历所有村庄,选出未访问、距离值最小的点 $t$;
    • 将该点对应边权累加至MST总权和,标记为已访问;
    • 以点 $t$ 为中介,更新其余未访问点的最小边权,边权使用分数规划构造的 $w=a-mid\cdot b$;
  3. 循环结束后判断最小生成树总权和是否小于等于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;
}
  1. 二分区间 $[0,10^7]$:高度最大 $10^7$,长度最小趋近于0,比值不会超过 $10^7$;
  2. 循环终止条件r-l>1e-6:预留足够精度,保证最终保留三位小数时四舍五入准确;
  3. 取区间中点mid调用判定函数:
    • prim(mid)=true:当前比值可行,收缩右边界,寻找更小比值;
    • prim(mid)=false:当前比值过小,收缩左边界,放大猜测值;
  4. 循环结束后返回最优浮点解。

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;
}
  1. ios::sync_with_stdio(false); cin.tie(0); 加速cin读取,大数据多组输入不超时;
  2. while(cin >> n && n != 0) 循环读取多组样例,读到 $n=0$ 直接结束程序;
  3. 读取全部村庄数据后,双重循环预处理所有点对的成本a与长度b,避免重复计算;
  4. 调用二分函数得到最优比值,printf("%.3lf\n")格式化输出,自动保留三位小数并换行。

六、算法复杂度分析

  1. 浮点二分:区间 $[0,10^7]$,精度 $10^{-6}$,二分循环约50次;
  2. 单次Prim最小生成树:$O(n^2)$,$n\le1000$,单次运算约100万次;
    总复杂度:$O(50 \times n^2)$,题目数据范围下运算量极小,无超时风险。

七、题型总结

  1. 模型定位:01分数规划 + 稠密图最小生成树综合题,是信息学竞赛经典中档综合算法;
  2. 固定解题流程
    ① 分式数学变形,构造新边权 $w=a-mid\cdot b$;
    ② 浮点二分枚举比值;
    ③ 使用Prim/Kruskal求最小生成树,判定总权和符号;
  3. 适用场景拓展:所有「最大化/最小化两类权值和的比值」且带图连通约束的题目,都可以套用这套分数规划+图论模板。

posted on 2026-07-08 12:53  5iCode  阅读(4)  评论(0)    收藏  举报

导航