[例题/总结]0/1分数规划
一、总述
0/1分数规划是专门解决0/1分数规划模型
的一种算法(废话)。所以说0/1分数规划模型是什么呢?给定整数{\(a_1,a_2,a_3,...,a_n\)},{\(b_1,b_2,b_3,...,b_n\)}从中选出若干对数,使得它们各自和的比值最大。公式如下:
二、实现原理
那么我们用什么方法可以求出这样一个看上去十分复杂的柿子呢?正确的答案是二分法,但是目前来看求解该式的最大值与二分法无关。
我们可以任意猜测一个比值\(Q\),此时分为两种情况讨论:
-
\(\exists\){\(x_1,x_2,...,x_n\)},使得\(\frac{\sum_{p=1}^{n}a_p\times x_p}{\sum_{p=1}^{n}b_p\times x_p}(x_p=1,0)\geqslant Q\)
通过计算发现存在解使得答案大于\(Q\),这说明此时我们的\(Q\)猜小了,\(Q\)还可以继续变大。 -
\(\forall\){\(x_1,x_2,...,x_n\)},使得\(\frac{\sum_{p=1}^{n}a_p\times x_p}{\sum_{p=1}^{n}b_p\times x_p}(x_p=1,0)\lt Q\)
所有的答案都比\(Q\)小,可以得出我们的\(Q\)枚举的大了,需要减少。
这个求解\(Q\)的过程是不是很熟悉?机智的你一定发现\(Q\)具有二分性,这和二分答案方法是一样的,至此我们可以用二分答案解决这个问题。
那么如何计算是否存在这样的比值大于我们枚举的\(Q\)呢?显然,直接计算这个比值是极其不明智的选择。这时需要我们对公式进行一个小小的变形。
观察这个式子:
我们把分母乘到等式右边:
在把右边的柿子移到左边来:
提取公因式得到最终公式:
现在我们知道怎么做了,由于只要存在一组解就可以,那么我们只要求出这个式子的最大值并判断这个值是否大于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!\n"):printf("%.3lf\n",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;
}