浅谈分数规划
关于 SPFA,他死了。 关于分数规划,他还活着。
——鲁迅
- 有用的分数规划模板。
由一个简单地问题引入。
有
使得:
中的
然后呢,题目问的就是这个最大的
这个题目由于
那么遇到这种问题要怎么做呢?
每个元素可以选或不选是吧,01 背包启动!
那么问题来了,假设我们知道了
那么我们考虑使用二分查找。找到最大的
那么我们设:
移项得:
设对于
则:
那么我们只要把
参考代码:
#include<bits/stdc++.h>
using namespace std;
double a[2000],b[2000];
int n,k;
double l,r;
const double eps=1e-8;
bool check(double mid)
{
double c[2000];
for(int i=1;i<=n;i++)c[i]=mid*b[i]-a[i];
sort(c+1,c+1+n);
double sum=0;
for(int i=1;i<=n;i++)sum+=c[i];
return (sum<=0);
}
main()
{
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
{
scanf("%lf%lf",&a[i],&b[i]);
r+=a[i];
}
while(r-l>eps)
{
double mid=(l+r)/2.0;
if(check(mid))l=mid;
else r=mid;
}
printf("%.4lf",l);
return 0;
}
上述问题中去除
这个条件的问题,也可以同类似的方法求解。而且甚至更加简单,如果对 check 函数长这样:
bool check(double mid)
{
double c[2000];
for(int i=1;i<=n;i++)c[i]=mid*b[i]-a[i];
double minn=1e9;
for(int i=1;i<=n;i++)minn=min(minn,c[i]);
return (minn<=0);
}
复杂度:
- 最优比例背包问题
【例题 1】洛谷 P4377 [USACO18OPEN] Talent Show G
首先来看为什么能从这个问题看出这是一道分数规划呢?原题语句:
帮助他确定这样的队伍中能够达到的最佳的才艺与重量的比值。
在原集合中选取一个子集,使得两个属性的比值最大,这便是分数规划的典型表达。这种问题,还添加了一个限制条件,那就是总重量之和不小于
还是用老方法进行分析,考虑二分查找一个
用同样的方法进行整理:
然后我们发现,每个奶牛的贡献就变成了
至于判断函数,选择使用刷表法进行 01 背包。具体实现思路为:
设
#include<bits/stdc++.h>
using namespace std;
int n,W,w[255],t[255];
double l,r,mid,y[255],dp[1002];
bool check(double x)
{
for(int i=1;i<=n;i++)y[i]=t[i]-x*w[i];
for(int i=0;i<=W;i++)dp[i]=-0x3f3f3f3f;
dp[0]=0;
for(int i=1;i<=n;i++)
for(int j=W;j>=0;j--)
if(j+w[i]>=W)dp[W]=max(dp[W],dp[j]+y[i]);
else dp[j+w[i]]=max(dp[j+w[i]],dp[j]+y[i]);
return dp[W]>=0;
}
main()
{
scanf("%d%d",&n,&W);
for(int i=1;i<=n;i++)scanf("%d%d",&w[i],&t[i]),r+=t[i];
while(r-l>=0.00000001)
{
mid=(l+r)/2.0;
if(check(mid))l=mid;
else r=mid;
}
printf("%d",(int)floor(mid*1000));
return 0;
}
- 最优比例生成树
【例题 2】洛谷 P4951 [USACO01OPEN] Earthquake
还是使用基本一致的套路分析。原题语句:
请帮助奶牛们选择修复哪些道路,才能使单位时间的利润最大?
单位时间的利润,指的是收益除以时间的比值。它要保证所有牧场联通,那么要保证边数最少,那么就要保证这是图的一棵生成树。
设有序列
设:
再设:
得:
移项:
那么我们就发现所有边中的第
于是,我们考虑建一个新图,新图每一条边两端的节点不变,第
具体实现如下:
#include<bits/stdc++.h>
#define int long long
#define N 402
#define M 11200
#define EPS 1e-10
using namespace std;
struct edge{int u,v;double c,t;}old[M];
struct Node{int u,v;double w;}e[M];
bool operator <(Node x,Node y){return x.w<y.w;}
int fath[N],n,m;
double f,l,r,mid;
void init()
{
for(int i=1;i<=n;i++)fath[i]=i;
}
int find(int k)
{
if(fath[k]==k)return k;
else return find(fath[k]);
}
int un(int k1,int k2)
{
fath[find(k1)]=find(k2);
}
double kruscal()
{
init();
sort(e+1,e+1+m);
int cnt=0;
double ret=0;
for(int i=1;cnt<n-1;i++)
{
int nowu=e[i].u,nowv=e[i].v;
if(find(nowu)==find(nowv))continue;
un(nowu,nowv);
ret+=e[i].w;
cnt++;
}
return ret;
}
int check(double x)
{
for(int i=1;i<=m;i++)e[i]=(Node){old[i].u,old[i].v,old[i].t*x+old[i].c};
return kruscal()<=f;
}
main()
{
scanf("%lld%lld%lf",&n,&m,&f);
for(int i=1;i<=m;i++)
scanf("%lld%lld%lf%lf",&old[i].u,&old[i].v,&old[i].c,&old[i].t);
r=0x3f3f3f3f;
while(r-l>=EPS)
{
mid=(l+r)/2;
if(check(mid))l=mid;
else r=mid;
}
printf("%.4lf",l);
return 0;
}
- 最优比例环
【例题 3】洛谷 P2868 [USACO07DEC] Sightseeing Cows G
这题稍微有点不一样。
这也是一道分数规划题,因为它求的是均值最小的环。
我们设
二分枚举出一个答案
因此,我们就转换成我们把原图的第
关于 spfa,他没死!!!!!
考虑把每一条边的权值取相反数,变成
#include<bits/stdc++.h>
#define N 11200
#define M 56000
using namespace std;
struct Node{int to,next;double w;}edge[M];
int cnt,head[N],ff[N],n,m,U[M],V[M],W[M];
int c[N],flag[N];
double d[N];
double l,r,mid;
void init()
{
for(int i=0;i<N;i++)head[i]=-1;
for(int i=0;i<M;i++)edge[i].next=-1;
cnt=0;
}
int addedge(int u,int v,double w)
{
edge[cnt]=(Node){v,head[u],w};
head[u]=cnt++;
}
bool spfa(int s)
{
for(int i=1;i<=n;i++)c[i]=flag[i]=0,d[i]=0x3f3f3f3f;
queue<int> q;
c[s]=1;
d[s]=0;
q.push(s);
flag[s]=1;
while(!q.empty())
{
int now=q.front();
q.pop();
flag[now]=0;
for(int i=head[now];~i;i=edge[i].next)
{
int t=edge[i].to;double tw=edge[i].w;
if(d[now]+tw<d[t])
{
d[t]=d[now]+tw;
if(!flag[t])
{
flag[t]=1;
if(c[t]>=n)return 1;
c[t]++;
q.push(t);
}
}
}
}
return 0;
}
bool check(double x)
{
init();
for(int i=1;i<=m;i++)addedge(U[i],V[i],x*W[i]-ff[U[i]]);
return spfa(1);
}
main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&ff[i]),r+=ff[i];
for(int i=1;i<=m;i++)scanf("%d%d%d",&U[i],&V[i],&W[i]);
for(int i=1;i<=30;i++)
{
mid=(l+r)/2;
if(check(mid))l=mid;
else r=mid;
}
printf("%.2f",l);
return 0;
}
最后总结一下, 分数规划并不是一个具体的算法,而是一种思路,就和 dp 一样。分数规划的难点在于建模,列不等式,对思维的要求较高,而对代码能力只有一般要求。
本文选用的题目多为来自某一个遥远的国度的 USACO 题目,国内考得并不多,甚至不是 NOI 大纲的考试范围。但是,这是我们学习其他算法时能够借鉴的。