01分数规划
此类题目求最优的比率,大意为:
有n件物品,a值为价值,b值为代价,要求你选择多件物品使得 达成最值(最大或最小)
那有同学就问了,如果求最大,直接选 最大的那个不就好了?
题目会让你怎么选吗?
如果要求选择多个的时候,不一定选 大的几个最优。这点可以自行证明。
设最大值为 ,那么得:
对于一种选法,对应一套 。
在假设一个答案 (v变为常量)后,假设有一种选法使得 ,也就说明为了使 , 需要变大。
如果求的是最大值,那么可以借此逐步放大 ,使之趋向于正确的答案。
这样做还有另外一个好处,当 变成常量后,每一个物品的 也是一个常量,那么如果要求最大值,只需要对所有物品的 进行排序,从大到小选择即可。
二分模板:
//题意为选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个,使 最大。
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算法在检验的时候,记录下当前( 为常量)最优选法下的更优的 值,带目的性的趋近。
上面例题的代码:
#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),两个点的距离为欧式距离(直线),代价为 。求这个最小生成树使得 最小。
解析:
既然要求的是 ,那么对于一条边,其连入后累加的应该是: 。
要求最小值,那么我们追求 。即所有边按照 从小到大排序,用克鲁斯卡尔算法做最小树即可。
这道题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
题意:
在一个有向图中选一个环,使得环上的点权和除以边权和最大,求这个比值。
解析:
首先分析答案环形状,不可能为两个环的交(有一部分重叠),这个很容易证明。既然是一个环,那么可以把每条边的终点捆绑到边上。
中,cost为点权,dis为边权,那么每条边的实际贡献为 。求最大比率,要求 ,那么我们要找的是一个正环。
转为 ,那么就相当于求负环的存在与否了。用较于优秀的判环算法+二分就可以了。
看了网上很多博客,没几个人的判环算法可以过洛谷的模板题,基本TLE,不信可以自己试试 洛谷3385
相较于原模版有几个改动:
- dis数组因为是double了,不能用memset 0x3f了
- 因为边的实际权值并不是输入的值,所以原来“添加非负边的反边”操作不能做(其实我也不知道为什么要添反边,但是不添确实会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;
}