题目链接:http://poj.org/problem?id=2728
题意:给出几个村庄的坐标x[i]和y[i],以及海拔z[i]。要在这些村庄之间建水渠,费用和两个村庄的海拔差成正比,水渠长度和村庄二维坐标(x,y)上的距离成正比,要求一种方案使得(总的花费/总的水渠长度)最小,输出这个最小值,保留三位小数。
这是一道0,1分数规划的题目,求的是一棵生成树sigma(dh)/sigma(l) 的最小值(dh是树上两点之间的高度差,l是树上两点之间的边长)。
按0,1分数规划的思路对解进行分析:(设解为R),有sigma(dh)-R*sigma(l)=0, 假设X不是最优解即不能使得(总的花费/总的水渠长度)最小,比R要大一点,那么最优比率生成树使得sigma(dh)/sigma(l) < X,也就是sigma(dh)-X*sigma(l)<0即sigma(dh-X*l)<0所以把两点间的权用dh-X*l来表示,所以必有一棵最小生成树使得生成树的权和<0。同理若X>R,则必有一棵最小生成树使得生成树的权和>0。
所以可以二分出答案。
因为这题是完全图,而且时间卡的很紧,一般用kruskal都T了,而且prim用堆优化也有很多T了,prim+堆复杂度是O( |E|log|V| ),
是完全图所以复杂度是O( V^2log|V| )反而比裸的primO(V^2)要大。
所以这是TLE的代码:
// 各种TLE ...
#include<cstdio>
#include<queue>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<vector>
#define inf 0x7fffffff
using namespace std;
struct node{int x,y,h;};
typedef pair<double,int >P;
priority_queue<P,vector<P>,greater<P> >que;
node point[1100];
int n;
double dis[1100];
bool vis[1100];
void ini()
{
for(int i=1;i<=n;i++){
scanf("%d%d%d",&point[i].x,&point[i].y,&point[i].h);
}
}
double dist(int u,int v,double x)
{
double dx=point[u].x-point[v].x,dy=point[u].y-point[v].y;
double ans=abs(point[u].h-point[v].h)-x*sqrt(dx*dx+dy*dy);
return ans;
}
bool ok_prim(double x)
{
memset(vis,0,sizeof(vis));
fill(dis,dis+n+1,inf);
double res=0;
dis[1]=0;
que.push(P(0,1));
while(!que.empty()){
P p=que.top();
que.pop();
int u=p.second;
if(vis[u]) continue;
vis[u]=1;
res+=dis[u];
for(int i=1;i<=n;i++){
if(vis[i]) continue;
double tmp=dist(u,i,x);
if(dis[i]>tmp){
dis[i]=tmp;
que.push(P(dis[i],i));
}
}
}
return res>=0;
}
void solve()
{
double lb=0,ub=1e5;
while(ub-lb>1e-4){
double mid=(lb+ub)/2;
if(ok_prim(mid)) lb=mid;
else ub=mid;
}
printf("%.3f\n",lb);
}
int main()
{
while(~scanf("%d",&n)&&n){
ini();
solve();
}
return 0;
}
用裸的prim二分才刚好过:
//Accepted 724K 2438MS
#include<cstdio>
#include<queue>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<vector>
#define inf 9999999999
using namespace std;
struct node{int x,y,h;};
node point[1100];
int n;
double dis[1100];
bool vis[1100];
void ini()
{
for(int i=1;i<=n;i++){
scanf("%d%d%d",&point[i].x,&point[i].y,&point[i].h);
}
}
double dist(int u,int v,double x) // 求两点间dh-X*l的边权
{
double dx=point[u].x-point[v].x,dy=point[u].y-point[v].y;
double ans=abs(point[u].h-point[v].h)-x*sqrt(dx*dx+dy*dy);
return ans;
}
bool ok_prim(double x)
{
memset(vis,0,sizeof(vis));
fill(dis,dis+n+1,inf);
double res=0;
dis[1]=0;
while(true){
int v=-1;
for(int u=1;u<=n;u++){
if(!vis[u]&&(v==-1||dis[u]<dis[v])) v=u;
}
if(v==-1) break;
vis[v]=1;
res+=dis[v];
for(int u=1;u<=n;u++){
if(vis[u]) continue;
dis[u]=min(dis[u],dist(v,u,x));
}
}
return res>=0;
}
void solve()
{
double lb=0,ub=1e7;
while(ub-lb>1e-4){ //二分
double mid=(lb+ub)/2;
if(ok_prim(mid)) lb=mid;
else ub=mid;
}
printf("%.3f\n",lb);
}
int main()
{
while(~scanf("%d",&n)&&n){
ini();
solve();
}
return 0;
}
可以看出二分的效率实在不高,0,1规划还有另一个算法 Dinkelbach 算法,类似于牛顿迭代法。算法的具体过程是:随便假设一个X1解(比如假设的这个解比最优解大),然后用同样的思路去求最小生成树,肯定会得到边权和<0,所以不是最优解,但这不是最终的目的,还需要用到这棵生成树,题目让我们求要求一种方案使得(总的花费/总的水渠长度)最小解,所以算出按这棵生成树的方案得到的解X2,这个解必定更靠近最优解(因为这棵树使得sigma(dh)/sigma(l) < X1,所以比X1要优),所以再用X2去求dh-X*l的最小生成树,再按这棵树的方式得到更优解X3,迭代多次后Xn,Xn-1与最优解相差无几,所以Xn-Xn-1<ESP,就求得了最优解。
以上这是这个算法的正确性的认识,但是复杂度的证明很复杂。但是比二分要优。
//732K 344MS
#include<cstdio>
#include<queue>
#include<iostream>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<vector>
using namespace std;
struct node{int x,y,h;};
node point[1100];
int n;
double dis[1100];
int pre[1100];
bool vis[1100];
void ini()
{
for(int i=1;i<=n;i++){
scanf("%d%d%d",&point[i].x,&point[i].y,&point[i].h);
}
}
double dist(int u,int v,double x)
{
double dx=point[u].x-point[v].x,dy=point[u].y-point[v].y;
double ans=abs(point[u].h-point[v].h)-x*sqrt(dx*dx+dy*dy);
return ans;
}
double prim(double x)
{
memset(vis,0,sizeof(vis));
double sh=0,sl=0;
dis[1]=0;
vis[1]=1;
for(int i=2;i<=n;i++){
dis[i]=dist(1,i,x);
pre[i]=1;
}
while(true){
int v=-1;
for(int u=1;u<=n;u++){
if(!vis[u]&&(v==-1||dis[u]<dis[v])) v=u;
}
if(v==-1) break;
vis[v]=1;
sh+=abs(point[v].h-point[pre[v]].h);
double dx=point[pre[v]].x-point[v].x,dy=point[pre[v]].y-point[v].y;
sl+=sqrt(dx*dx+dy*dy);
for(int u=1;u<=n;u++){
if(vis[u]) continue;
double tmp=dist(v,u,x);
if(dis[u]>tmp){
dis[u]=tmp;
pre[u]=v;
}
}
}
return sh/sl;
}
void solve()
{
double a=0,b=0;//随便设一个解0
while(1){
b = prim(a); //迭代
if(fabs(a-b)<1e-7) break;
a=b; //迭代
}
printf("%.3f\n",b);
}
int main()
{
while(~scanf("%d",&n)&&n){
ini();
solve();
}
return 0;
}