01分数规划(Dinkelbach算法、最优比率生成树、最优比率环)

01分数规划

此类题目求最优的比率,大意为:

有n件物品,a值为价值,b值为代价,要求你选择多件物品使得 a / b \sum a/\sum b 达成最值(最大或最小)

那有同学就问了,如果求最大,直接选 a i / b i a_i/b_i 最大的那个不就好了?
题目会让你怎么选吗?
如果要求选择多个的时候,不一定选 a i / b i a_i/b_i 大的几个最优。这点可以自行证明。

设最大值为 v v ,那么得: a / b = v a = v b a v b = 0 \sum a/\sum b=v\\\sum a=v*\sum b\\\sum a-v*\sum b=0

对于一种选法,对应一套 a b \sum a、\sum b

在假设一个答案 v v (v变为常量)后,假设有一种选法使得 a v b > 0 \sum a-v*\sum b>0 ,也就说明为了使 a v b = 0 \sum a-v*\sum b=0 v v 需要变大。

如果求的是最大值,那么可以借此逐步放大 v v ,使之趋向于正确的答案。

这样做还有另外一个好处,当 v v 变成常量后,每一个物品的 a i v b i a_i-vb_i 也是一个常量,那么如果要求最大值,只需要对所有物品的 a i v b i a_i-vb_i 进行排序,从大到小选择即可。

二分模板:

//题意为选n-k个
bool check(double r){
    for(int i=1;i<=n;i++)w[i]=a[i]-r*b[i];
    sort(w+1,w+1+n);
    double ans=0;
    //最大值时选大的  判断:>0为有更好的
    for(int i=min(k+1,n);i<=n;i++)ans+=w[i];
    if(ans>EPS)return true;
    return false;
    /*
    //最小值时选小的  判断:<0为有更好的
    for(int i=1;i<=n-k;i++)ans+=w[i];
    if(ans<-EPS)return true;
    return false;
    */
}

int main()
	...
	while(r-l>EPS){
            double mid=(l+r)/2;
            //最大值时 可以更大则拔高l
            if(check(mid))l=mid;
            else r=mid;
            /*
            //最小值时 可以更小则拉低r
            if(check(mid))r=mid;
            else l=mid;
            */
        }
        // r is ans
}

例题:最大比率裸体 Dropping tests

题意: n个物品(a、b值),至少选择n-k个,使 100 ( a / b ) 100*(\sum a/\sum b) 最大。

AC代码:

#include<stdio.h>
#include<string.h>
#include<math.h>
#include<iostream>
#include<algorithm>
using namespace std;

const double EPS=1e-4;
int n,k;
double a[1005],b[1005];
double w[1005];

bool check(double r){
    for(int i=1;i<=n;i++)w[i]=a[i]-r*b[i];
    sort(w+1,w+1+n);
    double ans=0;
    for(int i=min(k+1,n);i<=n;i++)ans+=w[i];
    if(ans>EPS)return true;
    return false;
}

int main(){
    while(scanf("%d%d",&n,&k)!=EOF){
        if(!n&&!k)break;
        double l=1e9,r=0;
        for(int i=1;i<=n;i++)scanf("%lf",a+i);
        for(int i=1;i<=n;i++)scanf("%lf",b+i),l=min(l,a[i]/b[i]),r=max(r,a[i]/b[i]);
        while(r-l>EPS){
            double mid=(l+r)/2;
            if(check(mid))l=mid;
            else r=mid;
        }
        printf("%.0f\n",r*100);
    }
}


Dinkelbach算法

这个算法是上面二分算法的优化。二分检验当前值与正确答案的关系,采取二分趋近的方式。Dinkelbach算法在检验的时候,记录下当前( v v 为常量)最优选法下的更优的 v v 值,带目的性的趋近。

上面例题的代码:

#include<stdio.h>
#include<string.h>
#include<math.h>
#include<iostream>
#include<algorithm>
using namespace std;

const double EPS=1e-4;
int n,k;
double a[1005],b[1005];
double w[1005];
pair<double,int>W[1005];//第二维记录选择的是哪个
/*
bool check(double r){
    for(int i=1;i<=n;i++)w[i]=a[i]-r*b[i];
    sort(w+1,w+1+n);
    double ans=0;
    for(int i=min(k+1,n);i<=n;i++)ans+=w[i];
    if(ans>EPS)return true;
    return false;
}*/

int main(){
    while(scanf("%d%d",&n,&k)!=EOF){
        if(!n&&!k)break;
        double l=1e9,r=0;
        for(int i=1;i<=n;i++)scanf("%lf",a+i);
        for(int i=1;i<=n;i++)scanf("%lf",b+i),l=min(l,a[i]/b[i]),r=max(r,a[i]/b[i]);
        /***********二分
        while(r-l>EPS){
            double mid=(l+r)/2;
            if(check(mid))l=mid;
            else r=mid;
        }
        printf("%.0f\n",r*100);
        ***************/

        //***********Dinkelbach
        double ans;
        while(1){
            ans=l;
            for(int i=1;i<=n;i++)
            	W[i].first=a[i]-l*b[i],W[i].second=i;
            sort(W+1,W+1+n);
            double A=0,B=0;
            for(int i=min(k+1,n);i<=n;i++)
            	A+=a[W[i].second],B+=b[W[i].second];
            l=A/B;
            if(fabs(ans-l)<EPS)break;
        }
        printf("%.0f\n",ans*100);
    }
}


例题:最优比率生成树 Desert King

题意:

n个点(x,y,h),两个点的距离为欧式距离(直线),代价为 Δ h \Delta h 。求这个最小生成树使得 c o s t / d i s \sum cost/\sum dis 最小。

解析:

既然要求的是 c o s t / d i s \sum cost/\sum dis ,那么对于一条边,其连入后累加的应该是: c o s t v d i s cost-v* dis

要求最小值,那么我们追求 c o s t v d i s &lt; 0 \sum cost-v*\sum dis&lt;0 。即所有边按照 c o s t v d i s cost-v* dis 从小到大排序,用克鲁斯卡尔算法做最小树即可。

这道题kruskal会慢很多,因为n个点n^2条边

就直接上Dinkelbach了

#include<stdio.h>
#include<string.h>
#include<math.h>
#include<iostream>
#include<algorithm>
using namespace std;
#define debug(i) printf("# %d\n",i)

const double EPS = 1e-4;
const int N = 1009;
const int M = 2000009;
int n;
double x[N],y[N],h[N];
struct node{
    int u,v;
    double val,L;
    node(){}
    node(int u,int v,double val,double L):v(v),u(u),val(val),L(L){}
    bool operator <(const node &r)const{
        return val<r.val;
    }
}e[M];

int fa[N];
int fi(int p){return fa[p]==p?p:fa[p]=fi(fa[p]);}

int main(){
    while(scanf("%d",&n)!=EOF){
        if(n==0)break;
        for(int i=1;i<=n;i++)
            scanf("%lf%lf%lf",x+i,y+i,h+i);

        double ans=0,l=0;
        while(1){
            ans=l;
            for(int i=1;i<=n;i++)fa[i]=i;
            int now=0;
            for(int i=1;i<=n;i++){
                for(int j=i+1;j<=n;j++){
                    double L=sqrt((y[i]-y[j])*(y[i]-y[j])+(x[i]-x[j])*(x[i]-x[j]));
                    e[++now]=node(i,j,fabs(h[i]-h[j])-l*L,L);
                }
            }
            double A=0,B=0;
            sort(e+1,e+1+now);
            for(int i=1;i<=now;i++){
                int f1=fi(e[i].u),f2=fi(e[i].v);
                if(f1==f2)continue;
                fa[f1]=f2;
                A+=fabs(h[e[i].u]-h[e[i].v]);
                B+=e[i].L;
            }
            l=A/B;
            if(fabs(ans-l)<EPS)break;
        }
        printf("%.3f\n",ans);
    }
}

例题:最优比率环 Sightseeing Cows

题意:

在一个有向图中选一个环,使得环上的点权和除以边权和最大,求这个比值。

解析:

首先分析答案环形状,不可能为两个环的交(有一部分重叠),这个很容易证明。既然是一个环,那么可以把每条边的终点捆绑到边上。

c o s t / d i s \sum cost/\sum dis 中,cost为点权,dis为边权,那么每条边的实际贡献为 c o s t v d i s cost-v*dis 。求最大比率,要求 c o s t v d i s &gt; 0 \sum cost-v*\sum dis&gt;0 ,那么我们要找的是一个正环。

转为 v d i s c o s t v*dis-cost ,那么就相当于求负环的存在与否了。用较于优秀的判环算法+二分就可以了。

看了网上很多博客,没几个人的判环算法可以过洛谷的模板题,基本TLE,不信可以自己试试 洛谷3385

相较于原模版有几个改动:

  1. dis数组因为是double了,不能用memset 0x3f了
  2. 因为边的实际权值并不是输入的值,所以原来“添加非负边的反边”操作不能做(其实我也不知道为什么要添反边,但是不添确实会WA,这里过了可能是数据水吧)
#include<stdio.h>
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int maxn=5050;
const double EPS=1e-4;
struct Edge{
    int u,v,nxt;
    double w;
}edge[maxn<<1];
int fst[maxn],cnt;
inline void addedge(int u,int v,double w){
    edge[++cnt].u=u,edge[cnt].v=v,edge[cnt].w=w,
    edge[cnt].nxt=fst[u],fst[u]=cnt;
}
int n,m;
double P[maxn];
double dis[maxn];
int ins[maxn];
int check(double Val){
    for(int i=1;i<=n;i++)dis[i]=1e11;
    memset(ins,0,sizeof(ins));
    queue<int>q;
    q.push(1);
    int num[maxn];
    ins[1]=1;
    num[1]=1;
    dis[1]=0;
    while(!q.empty()){
        int u=q.front();q.pop();
        ins[u]=0;
        for(int i=fst[u];i;i=edge[i].nxt){
            int v=edge[i].v;
            if(dis[v]>dis[u]+Val*edge[i].w-P[v]){
                dis[v]=dis[u]+Val*edge[i].w-P[v];
                num[v]=num[u]+1;
                if(num[v]>n)return 1;
                if(!ins[v]){
                    ins[v]=1;
                    q.push(v);
                }
            }
        }
    }
    return 0;
}
int main() {
    scanf("%d%d", &n, &m);
    for(int i=1;i<=n;i++)
        scanf("%lf",P+i);
    for(int i=1;i<=m;i++){
        int a,b;double c;scanf("%d%d%lf",&a,&b,&c);
        addedge(a,b,c);
    }

    // 无环,或者可以dfs一遍判断
    // 这样使所有边为负,有环则一定为负环
    if(!check(-1e5))return 0*printf("0\n");

    double l=0,r=1e5;
    while(r-l>EPS){
        double mid=(l+r)/2;
        if(check(mid))l=mid;
        else r=mid;
    }
    printf("%.2f\n",r);
    return 0;
}

猜你喜欢

转载自blog.csdn.net/jk_chen_acmer/article/details/87608367
今日推荐