[例题/总结]0/1分数规划


一、总述

0/1分数规划是专门解决0/1分数规划模型的一种算法(废话)。所以说0/1分数规划模型是什么呢?给定整数{(a_1,a_2,a_3,...,a_n)},{(b_1,b_2,b_3,...,b_n)}从中选出若干对数,使得它们各自和的比值最大。公式如下:

[frac{sum_{p=1}^{n}a_p imes x_p}{sum_{p=1}^{n}b_p imes x_p}(x_p=1,0) ]

二、实现原理

那么我们用什么方法可以求出这样一个看上去十分复杂的柿子呢?正确的答案是二分法,但是目前来看求解该式的最大值与二分法无关。
我们可以任意猜测一个比值(Q),此时分为两种情况讨论:

  1. (exists){(x_1,x_2,...,x_n)},使得(frac{sum_{p=1}^{n}a_p imes x_p}{sum_{p=1}^{n}b_p imes x_p}(x_p=1,0)geqslant Q)
    通过计算发现存在解使得答案大于(Q),这说明此时我们的(Q)猜小了,(Q)还可以继续变大。

  2. (forall){(x_1,x_2,...,x_n)},使得(frac{sum_{p=1}^{n}a_p imes x_p}{sum_{p=1}^{n}b_p imes x_p}(x_p=1,0)lt Q)
    所有的答案都比(Q)小,可以得出我们的(Q)枚举的大了,需要减少。

这个求解(Q)的过程是不是很熟悉?机智的你一定发现(Q)具有二分性,这和二分答案方法是一样的,至此我们可以用二分答案解决这个问题。

那么如何计算是否存在这样的比值大于我们枚举的(Q)呢?显然,直接计算这个比值是极其不明智的选择。这时需要我们对公式进行一个小小的变形。
观察这个式子:

[frac{sum_{p=1}^{n}a_p imes x_p}{sum_{p=1}^{n}b_p imes x_p}(x_p=1,0)geqslant Q ]

我们把分母乘到等式右边:

[sum_{p=1}^{n}a_p imes x_pgeqslant sum_{p=1}^{n}Q imes b_p imes x_p ]

在把右边的柿子移到左边来:

[sum_{p=1}^{n}a_p imes x_p - sum_{p=1}^{n}Q imes b_p imes x_pgeqslant 0 ]

提取公因式得到最终公式:

[sum_{p=1}^{n}(a_p-Q imes b_p) imes x_pgeqslant 0 ]

现在我们知道怎么做了,由于只要存在一组解就可以,那么我们只要求出这个式子的最大值并判断这个值是否大于0。大于0说明(Q)不够大,小于0说明(Q)太大了,用二分法不断逼近答案直到达到合适的精度。

三、例题

以下例题不是十分困难,稍复杂的地方就是二分法check( )函数的写法。

例1:POJ2976 Dropping tests(原POJ2519)

题目疯狂明示让你用01分数规划。二分枚举成绩,求出每一项的值并排序(要求最大值),如果答案小于0那么更改左区间,反之更改右区间。
Code:

#include<bits/stdc++.h>
#define N 2000
using namespace std;
int n,k;
double a[N],b[N],f[N];
double check(double mid)
{
	memset(f,0,sizeof(f));
	for(int i=1;i<=n;i++)
		f[i]=a[i]-mid*b[i];//转化后的公式
	sort(f+1,f+n+1,greater<double>());
	double sum=0;
	for(int i=1;i<=n-k;i++)
		sum+=f[i];//求一下最大值
	return (sum>0)? 1:0;
}
int main()
{
	while(scanf("%d%d",&n,&k)&&n+k)
	{
		memset(a,0,sizeof(a));
		memset(b,0,sizeof(b));
		for(int i=1;i<=n;i++) scanf("%lf",&a[i]);
		for(int i=1;i<=n;i++) scanf("%lf",&b[i]);
		double l=0,r=1e10;
		while(r-l>1e-8){
			double mid=(l+r)/2;
			if(check(mid)) l=mid;
			else r=mid;
		}
		cout<<fixed<<setprecision(0)<<l*100<<endl;
	}
	return 0;
}

例2:P1730 最小密度路径

对所有点对进行一次01分数规划,接下来跑最短路判断枚举的密度是否可行,最终求得最小密度路径。

#include<bits/stdc++.h>
#define N 10010
#define INF 0x3f3f3f3f
#define eps 1e-6
#define ll long long
using namespace std;
double ans[N][N],dist[N],maxn,cost[N];
int q,n,m,tot,vis[N];
int first[N],go[N],next[N];
inline void add_edge(int u,int v,double w){
	next[++tot]=first[u];
	first[u]=tot;
	go[tot]=v;
	cost[tot]=w;
}
inline int check(int s,int ed,double mid){
	queue<int> q;
	for(int i=1;i<=n;i++){
		dist[i]=INF;vis[i]=0;
	}
	q.push(s);vis[s]=1;dist[s]=0;
	while(!q.empty()){
		int u=q.front();
		q.pop();vis[u]=0;
		for(int e=first[u];e;e=next[e]){
			int v=go[e];double w=cost[e];
			if(dist[v]>dist[u]+w-mid){
				dist[v]=dist[u]+w-mid;
				if(!vis[v]){
					q.push(v);
					vis[v]=1;
				}
			}
		}
	}
	return (dist[ed]>0)? 1:0;//求的最小比值大于枚举值,更新l,否则更新r
}
inline void erfen(){
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++){
        	if(i==j) continue;
			check(i,j,0);
            if(dist[j]==INF){ans[i][j]=-1;continue;}
            long double l=0,r=maxn;
            while(r-l>eps){
                long double mid=(l+r)/(2.0);
                if(!check(i,j,mid)) r=mid;
                else l=mid;
            }
            ans[i][j]=l;
        }
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1,u,v,w;i<=m;i++){
		scanf("%d%d%d",&u,&v,&w);
		add_edge(u,v,(double)w);
		maxn+=w;
	}
	erfen(); 
	scanf("%d",&q);
	for(int i=1,u,v;i<=q;i++){
		scanf("%d%d",&u,&v);
		ans[u][v]<0?printf("OMG!
"):printf("%.3lf
",ans[u][v]);
	}
	return 0;
}

例3:CF489E Hiking

基础题。
Code:

#include<bits/stdc++.h>
#define N 100010
using namespace std;
const double INF=1e15;
int n,last[N];
double len,pos[N],w[N],f[N];
vector<int> v;
double check(double mid)
{
	for(int i=1;i<=n;i++){
		f[i]=INF;
		for(int j=0;j<i;j++){
			if(f[i]>f[j]+sqrt(fabs(pos[i]-pos[j]-len))-mid*w[i]){
				f[i]=f[j]+sqrt(fabs(pos[i]-pos[j]-len))-mid*w[i];
				last[i]=j;
			}
		}
	}
	return (f[n]<=0)?1:0;
}
int main()
{
	scanf("%d%lf",&n,&len);
	for(int i=1;i<=n;i++)
		scanf("%lf%lf",&pos[i],&w[i]);
	double l=0,r=1e10;
	while(r-l>=1e-9){
		double mid=(l+r)/2;
		if(check(mid)) r=mid;
		else l=mid;
	}
	check(l);
	int now=n;
	while(now>0){
		v.push_back(now);
		now=last[now];
	}
	for(int i=v.size()-1;i>=0;i--)
		printf("%d ",v[i]);
	return 0;
}

例4:P2868 [USACO07DEC]观光奶牛Sightseeing Cows

此题同P1768 天路类似。
奶牛们最终要回到起点,我们同样枚举一个比值(Q),可以知道如果变形后不等式大于0,也就是说存在环,那么更新左区间端点(l)。判断一个环可以转换成负环处理,即用SPFA判负环。

#include<bits/stdc++.h> 
#define N 200010
using namespace std;
int first[N],next[N],go[N],cost[N],vis[N];
int m,n,tot;
double dist[N],len[N],f[N];
inline void add_edge(int u,int v,int w){
    next[++tot]=first[u];
    first[u]=tot;
    go[tot]=v;
    cost[tot]=w;
}
double SPFA(int u)//判负环
{
    vis[u]=1;
    for(int i=first[u];i;i=next[i])
    {
        int v=go[i];
		double w=len[i];
        if(dist[v]>dist[u]+w)
        {
            dist[v]=dist[u]+w;
            if(vis[v]||SPFA(v)){
                vis[v]=0;
                return 1;
            }
        }
    }
	vis[u]=0;
	return 0;
}
double check(double mid)
{
	for(int i=1;i<=tot;i++)
		len[i]=(double)cost[i]*mid-f[go[i]];
    for(int i=1;i<=n;i++){
		if(SPFA(i)) return 1;
	}
    return 0;
}
int main()
{
	scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
		scanf("%lf",&f[i]);
    for(int i=1;i<=m;i++){
    	int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
        add_edge(u,v,w);
    }
    double l=0,r=1e6;
    while(r-l>1e-6){
        double mid=(l+r)/2;
        if(check(mid))
			l=mid;
        else r=mid;
    }
    cout<<fixed<<setprecision(2)<<l<<endl;
    return 0;
}

pic.png

原文地址:https://www.cnblogs.com/cyanigence-oi/p/11766432.html