01分数规划入门

01分数规划是这样的一类问题,有一堆物品,每一个物品有一个收益ai,一个代价bi,我们要求一个方案使选择的$\sum{a_i}/\sum{b_i}$最大。

首先我们来一道例题吧,01分数规划的大体方法都是一样的。

例1 Dropping Tests poj2976

给出n个物品,每个物品有两个属性a和b,选择n-k个元素,询问$\sum{a_i}/\sum{b_i}$的最大值。

1<=n<=1000,0<=k<n,0<=ai<=bi<=1000000000。

首先这题显然是兹磁二分的,而$\sum{a_i}/\sum{b_i} \geq x$就等价于$\sum{a_i}-x\sum{b_i} \geq 0$。

所以我们发现二分完把ai-x*bi排序后把最大的n-k个选出来就行了。

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <algorithm>
#include <string.h>
#include <vector>
#include <math.h>
#include <limits>
#include <set>
#include <map>
using namespace std;
int n,k,a[2333],b[2333];
double ps[2333];
bool ok(double x)
{
    for(int i=1;i<=n;i++) ps[i]=a[i]-x*b[i];
    sort(ps+1,ps+1+n);
    double ans=0;
    for(int i=n;i>=k+1;i--) ans+=ps[i];
    return ans>=0;
}
void sol()
{
    for(int i=1;i<=n;i++) scanf("%d",a+i);
    for(int i=1;i<=n;i++) scanf("%d",b+i);
    double l=0,r=1;
    while(r-l>1e-6)
    {
        double mid=(l+r)/2;
        if(ok(mid)) l=mid; else r=mid;
    }
    printf("%.0lf\n",l*100);
}
int main()
{
    while(scanf("%d%d",&n,&k),n|k) sol();
}

但是还有一种神奇的做法,既然我们可以二分一个值,而且得出一个更优的解,那么我们就可以在这个解的基础上继续做,直到满足精度。

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <algorithm>
#include <string.h>
#include <vector>
#include <math.h>
#include <limits>
#include <set>
#include <map>
using namespace std;
int n,k;
struct pro {double a,b,p;}s[233333];
bool operator < (pro a,pro b) {return a.p<b.p;}
double sol(double x)
{
    for(int i=1;i<=n;i++) s[i].p=s[i].a-x*s[i].b;
    sort(s+1,s+1+n);
    double aa=0,bb=0;
    for(int i=n;i>=k+1;i--) aa+=s[i].a,bb+=s[i].b;
    return aa/bb;
}
void sol()
{
    for(int i=1;i<=n;i++) scanf("%lf",&s[i].a);
    for(int i=1;i<=n;i++) scanf("%lf",&s[i].b);
    double l=0,ans=0;
    do
    {
        l=ans; ans=sol(ans);
    }while(fabs(l-ans)>1e-6);
    if(l>1) l=1;
    if(l<0) l=0;
    printf("%.0lf\n",l*100);
}
int main()
{
    while(scanf("%d%d",&n,&k),n|k) sol();
}

例2 Desert King poj2728

(模型有转化)

给出一个n个点的完全图,每条边有两个权值cost和len。

求一个生成树使$\frac{\sum{cost_i}}{\sum{len_i}}$最小。

2<=n<=1000

还是二分答案x,把每条边边权设为cost-x*len,跑最小生成树,判答案的正负。建议用prim跑,复杂度比较科学。

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <algorithm>
#include <string.h>
#include <vector>
#include <math.h>
#include <limits>
#include <set>
#include <map>
using namespace std;
typedef long double ld;
#define SZ 1066
int n,xx[SZ],yy[SZ],hh[SZ],fm[SZ];
bool del[SZ];
ld mind[SZ],d[SZ][SZ],d1[SZ][SZ],d2[SZ][SZ];
ld sqr(ld x) {return x*x;}
ld Abs(ld x) {return (x>=0)?x:-x;}
bool ok(ld x)
{
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++) d[i][j]=d1[i][j]-x*d2[i][j];
    }
    for(int i=0;i<=n;i++) mind[i]=1000000000, del[i]=0;
    mind[1]=0; fm[1]=0;
    ld ans=0;
    for(int i=1;i<=n;i++)
    {
        int mp=0;
        for(int j=1;j<=n;j++)
        {
            if(mind[j]<mind[mp]&&!del[j]) mp=j;
        }
        ans+=d[fm[mp]][mp]; del[mp]=1;
        for(int j=1;j<=n;j++)
        {
            if(d[mp][j]>=mind[j]||del[j]) continue;
            mind[j]=d[mp][j]; fm[j]=mp;
        }
    }
    return ans<=0;
}
void sol()
{
    for(int i=1;i<=n;i++) scanf("%d%d%d",xx+i,yy+i,hh+i);
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            d1[i][j]=Abs(hh[i]-hh[j]);
            d2[i][j]=sqrt(sqr(xx[i]-xx[j])+sqr(yy[i]-yy[j]));
        }
    }
    ld l=0,r=1000000000;
    while(r-l>1e-6)
    {
        double mid=(l+r)/2;
        if(ok(mid)) r=mid; else l=mid;
    }
    printf("%.3lf\n",(double)l);
}
int main()
{
    while(scanf("%d",&n),n) sol();
}

直接迭代

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <algorithm>
#include <string.h>
#include <vector>
#include <math.h>
#include <limits>
#include <set>
#include <map>
using namespace std;
#define SZ 1066
int n,xx[SZ],yy[SZ],hh[SZ],fm[SZ];
bool del[SZ];
double mind[SZ],d[SZ][SZ],d1[SZ][SZ],d2[SZ][SZ];
double sqr(double x) {return x*x;}
double Abs(double x) {return (x>=0)?x:-x;}
double sol(double x)
{
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++) d[i][j]=d1[i][j]-x*d2[i][j];
    }
    for(int i=0;i<=n;i++) mind[i]=1000000000, del[i]=0;
    mind[1]=0; fm[1]=0;
    double aa=0,bb=0;
    for(int i=1;i<=n;i++)
    {
        int mp=0;
        for(int j=1;j<=n;j++)
        {
            if(mind[j]<mind[mp]&&!del[j]) mp=j;
        }
        aa+=d1[fm[mp]][mp];
        bb+=d2[fm[mp]][mp];
        del[mp]=1;
        for(int j=1;j<=n;j++)
        {
            if(d[mp][j]>=mind[j]||del[j]) continue;
            mind[j]=d[mp][j]; fm[j]=mp;
        }
    }
    return aa/bb;
}
void sol()
{
    for(int i=1;i<=n;i++) scanf("%d%d%d",xx+i,yy+i,hh+i);
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            d1[i][j]=Abs(hh[i]-hh[j]);
            d2[i][j]=sqrt(sqr(xx[i]-xx[j])+sqr(yy[i]-yy[j]));
        }
    }
    double l,ans=0;
    do{ans=sol(l=ans);}while(fabs(l-ans)>1e-6);
    printf("%.3lf\n",l);
}
int main()
{
    while(scanf("%d",&n),n) sol();
}

例3 Sightseeing Cows poj3621

给出一个有L个点P条边的有向图,每个点上有点权F,每条边上有边权T。

求一个回路使$\frac{\sum{F_i}}{\sum{T_i}}$最大。

2<=L<=1000,2<=P<=5000。

因为环上边和点数相等,我们可以直接把边权当做花费,入点的点权当作收入。

然后还是二分答案建图,判图中有没有正环。取个反就成了判负环。

dfs版spfa即可。

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <algorithm>
#include <string.h>
#include <vector>
#include <math.h>
#include <limits>
#include <set>
#include <map>
using namespace std;
#define SZ 666666
int n,m,v[SZ],fst[SZ],vb[SZ],nxt[SZ],vc[SZ],M=0,X;
bool vis[SZ],fh=0;
double dist[SZ];
void adde(int a,int b,int c) {++M; nxt[M]=fst[a]; fst[a]=M; vb[M]=b; vc[M]=c;}
void dfs(int p,double x)
{
    vis[p]=1;
    for(int e=fst[p];e;e=nxt[e])
    {
        double C=v[vb[e]]-x*vc[e];
        if(dist[vb[e]]>=C+dist[p]) continue;
        if(vis[vb[e]]) {fh=1; return;}
        dist[vb[e]]=C+dist[p];
        dfs(vb[e],x);
        if(fh) return;
    }
    vis[p]=0;
}
bool ok(double x)
{
    for(int i=1;i<=n;i++) vis[i]=dist[i]=0;
    fh=0;
    for(int i=1;i<=n;i++)
    {
        dfs(i,x);
        if(fh) return 1;
    }
    return 0;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",v+i);
    for(int i=1;i<=m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        adde(a,b,c);
    }
    double l=0,r=1000000000;
    while(r-l>1e-6)
    {
        double mid=(l+r)/2;
        if(ok(mid)) l=mid; else r=mid;
    }
    printf("%.2lf\n",l);
}
迭代的话…似乎不太好写,就算了吧
posted @ 2016-05-01 13:07  fjzzq2002  阅读(5849)  评论(2编辑  收藏  举报