01分数规划
0/1分数规划
0/1分数规划
给定一些物品,每个物品有 a[i],b[i] ,要求选 k 个,最大化 (∑ a[i])/(∑b[i]) 。
二分,判定答案是否能 ≥mid 。
那么即 (∑a[i])/(∑b[i])≥mid ,也即 ∑a[i]-mid×b[i] ≥0 。
将物品按照 a[i]-mid×b[i] 排序,若前 k 大的和 ≥0 ,那么答案 ≥mid 。
最大化的时候,假设最大值为r,那么就有∑pi / ∑si <= r,变形之后就有∑si * r* - ∑pi >= 0.
最小化的时候,假设最小值为r,那么就有∑pi / ∑si >= r,变形之后就有∑pi - ∑si * r* >= 0.
Dinkelbach算法
https://blog.csdn.net/Cassie_zkq/article/details/81477153
例:poj2976
选取n-k(原题是不选k)个物品使得比率最大。
阿西吧Poj每次都要输出%f才行
#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
#define eps 1e-6
const int N = 10005;
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return f*x;
}
int n,k;
struct node{
double a,b,d;
bool operator < (const node &x) const {
return d>x.d;
}
}t[N];
double l,r,mid;
bool check(double x) {
double sum=0;
for(int i=1;i<=n;i++) t[i].d=t[i].a-x*t[i].b;
sort(t+1,t+1+n);
for(int i=1;i<=n-k;i++) sum+=t[i].d;
return sum>=0;
}
int main(){
while(1) {
n=read();k=read();
if(!n && !k)return 0;
for(int i=1;i<=n;i++) scanf("%lf",&t[i].a);
for(int i=1;i<=n;i++) scanf("%lf",&t[i].b);
l=0,r=1e10;
while(fabs(r-l)>eps) {
mid=(l+r)/2.0;
if(check(mid)) l=mid;
else r=mid;
}
printf("%.0lf\n",(l*100.0));
}
}
例:Talent Show G
01背包+分数规划,01背包价值就是 d ,体积是 w ,最后看f[W] 是不是大于等于0就行
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
int n,m;
int w[300],t[300];
long long f[10000];
bool check(int z){
memset(f,128,sizeof(f));
f[0]=0;
long long tmp=f[m];
for(int i=1;i<=n;i++)
for(int j=m;j>=0;j--) {
int jj=min(m,j+w[i]);
f[jj]=max(f[jj],f[j]+t[i]-(long long)w[i]*z);
}
return f[m]>=0;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) {
scanf("%d%d",&w[i],&t[i]);
t[i]*=1000;
}
int l=0,r=1000000,ans;
while(l<=r){
int mid=l+r>>1;
if(check(mid)) ans=mid,l=mid+1;
else r=mid-1;
}
printf("%d",ans);
return 0;
}
最优比率生成树
Poj2728Desert King
依然二分,每条边的 a[i]-mid×b[i] 视作权值。
因为要求最小,直接求最小生成树即可。判断一下最小生成树的权值和
这道题是完全图,所以我们需要用prim求最小生成树
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return f*x;
}
const int N = 1006;
const int inf = 0x3f3f3f3f;
const double eps = 1e-6;
int n;
double cost[N][N],val[N][N],c[N][N],mi[N];
int x[N],y[N],z[N];
inline double get(int i,int j) {
return sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]));
}
bool vis[N];
bool check(double mid) {
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j++)
if(i==j) c[i][j]=c[j][i]=inf;
else c[i][j]=c[j][i]=cost[i][j]-mid*val[i][j];
memset(vis,0,sizeof(vis));
for(int i=1;i<=n;i++) mi[i]=inf;
mi[1]=0;
double ans=0;
while(1) {
int x=0;
for(int i=1;i<=n;i++)//每次选出最小的边权,共n-1条边,第一次是零为了更新边权
if(!vis[i]&&(!x||mi[x]>mi[i]))
x=i;
if(!x) break;
vis[x]=1;
ans+=mi[x];
for(int i=1;i<=n;i++) mi[i]=min(mi[i],c[x][i]);
}
return ans>=0;
}
int main() {
while(1) {
n=read();
if(!n) return 0;
for(int i=1;i<=n;i++)
x[i]=read(),y[i]=read(),z[i]=read();
double l=0,r=0,mid;
for(int i=1;i<=n;i++)
for(int j=i+1;j<=n;j++) {
r+=(cost[i][j]=cost[j][i]=abs(z[i]-z[j]));
val[i][j]=val[j][i]=get(i,j);
}
while((r-l)>=eps) {
mid=(l+r)/2.0;
if(check(mid)) l=mid;
else r=mid;
}
printf("%.3lf\n",l);
}
}
最优比率生成环
Sightseeing Cows G
Poj3621
Description
给定一张图,边上有花费,点上有收益,点可以多 次经过,但是收益不叠加,边也可以多次经过,但是费用 叠加。求一个环使得收益和/花费和最大,输出这个比值。
Solution
比上面更加的恶心了。先不说环的问题,就是花费和收益不在一处也令人蛋疼。这时候需要用到几个转化和 结论。
首先的一个结论就是,不会存在环套环的问题,即最优的方 案一定是一个单独的环,而不是大环套着小环的形式。这个的 证明其实非常的简单(提示,将大环上 的收益和记为x1,花费为y1,小环上的为x2,y2。重叠部分的花费为S。表示出来分类讨论即可)。有了这个结论,我们就可以 将花费和收益都转移到边上来了,因为答案最终一定是一个环, 所以我们将每一条边的收益规定为其终点的收益,这样一个环 上所有的花费和收益都能够被正确的统计。
解决了这个问题之后,就是01分数规划的部分了,我们只需要计算出D数组后找找有没有正权环即可,不过这样不太好, 不是我们熟悉的问题,将D数组全部取反之后,问题转换为查找有没有负权环,用spfa即可。
#include <queue>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
inline int read() {
int x=0;char ch=getchar();
while(!isdigit(ch)) ch=getchar();
while(isdigit(ch)) {x=x*10+ch-'0';ch=getchar();}
return x;
}
const double eps=1e-5;
const int M=10005;
const int N=2005;
int n,m;
int fun[N];
struct node{
int to,w,nxt;
double d;
}e[M];
int hd[M],tot;
inline void add(int x,int y,int z) {
e[++tot].to=y;e[tot].w=z;e[tot].nxt=hd[x];hd[x]=tot;
}
queue<int>q;
bool vis[N];
int cnt[N];
double dis[N];
bool check(double mid) {
for(int i=1;i<=tot;i++)
e[i].d=(double)mid*e[i].w-fun[e[i].to];
while(q.size()) q.pop();
for(int i=1;i<=n;++i){//因为图不一定连通,所以初始所有结点入队
q.push(i);
dis[i]=0; vis[i]=cnt[i]=1;
}
while(!q.empty()) {
int x=q.front();q.pop();
vis[x]=0;
for(int i=hd[x];i;i=e[i].nxt) {
int y=e[i].to;
if(dis[y]>dis[x]+e[i].d){
dis[y]=dis[x]+e[i].d;
if(!vis[y]) {vis[y]=1,q.push(y);if(++cnt[y]>=n) return 1;}
}
}
}
return 0;
}
int main() {
n=read();m=read();
for(int i=1;i<=n;i++) fun[i]=read();
for(int i=1,x,y,z;i<=m;i++) {
x=read();y=read();z=read();
add(x,y,z);//有向图
}
double l=0,r=100000,mid;
while((r-l)>eps) {
mid=(l+r)/2.0;
if(check(mid)) l=mid;
else r=mid;
}
printf("%.2lf",l);
return 0;
}
天路
板子题but ----T飞了好几次
if((cnt[y]=cnt[x]+1)>=n) return 1;要写成这样
而不是这样 if(++cnt[y]>=n) return 1;
#include <queue>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
inline int read() {
int x=0;char ch=getchar();
while(!isdigit(ch)) ch=getchar();
while(isdigit(ch)) {x=x*10+ch-'0';ch=getchar();}
return x;
}
const double eps=1e-5;
const int M=40005;
const int N=7005;
int n,m;
int fun[N];
struct node{
int to,nxt,v,p;
double d;
}e[M];
int hd[M],tot;
inline void add(int x,int y,int v,int p) {
e[++tot].to=y;e[tot].v=v;e[tot].p=p;e[tot].nxt=hd[x];hd[x]=tot;
}
bool vis[N];
int cnt[N];
double dis[N];
bool check(double mid) {
queue<int>q;
for(int i=1;i<=n;++i){//因为图不一定连通,所以初始所有结点入队
q.push(i);
dis[i]=0; vis[i]=cnt[i]=1;
}
while(!q.empty()) {
int x=q.front();q.pop();
vis[x]=0;
for(int i=hd[x];i;i=e[i].nxt) {
int y=e[i].to;
if(dis[y]>dis[x]+(double)mid*e[i].p-e[i].v){
dis[y]=dis[x]+(double)mid*e[i].p-e[i].v;
if(!vis[y]) {vis[y]=1,q.push(y);if((cnt[y]=cnt[x]+1)>=n) return 1;}
}
}
}
return 0;
}
int main() {
n=read();m=read();
for(int i=1,x,y,v,p;i<=m;i++) {
x=read();y=read();v=read();p=read();
add(x,y,v,p);//有向图
}
double l=0,r=200,mid;
bool f=0;
while(abs(r-l)>eps) {
mid=(l+r)/2.0;
if(check(mid)) f=1,l=mid;
else r=mid;
}
if(!f) puts("-1");
else printf("%.1lf",l);
return 0;
}
各种例题:
规划
树型背包,求树上一个n - m的连通块,使块中的点的权值之和最大。
令\(dp[u][j]\)表示以u为根的子树中,有一个点数为 j 的联通块时的最大值。这个联通块一定是包含u的。
动态转移方程:$$ f[u][j]=Max(f[u][j],f[u][j-k]+f[v][k]); $$
u是当前节点,v是儿子,j和k是要自己枚举的
#include <queue>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
inline int read() {
int x=0;char ch=getchar();
while(!isdigit(ch)) ch=getchar();
while(isdigit(ch)) {x=x*10+ch-'0';ch=getchar();}
return x;
}
const double eps=1e-5;
const int M=1000;
const int N=1000;
int n,m;
int a[N],b[N];
struct node{
int to,nxt;
double d;
}e[M];
int hd[M],tot;
inline void add(int x,int y) {
e[++tot].to=y;e[tot].nxt=hd[x];hd[x]=tot;
}
bool vis[N];
int cnt[N];
double d[N],f[N][N];
inline void dfs(int x,int fa) {
f[x][0]=0;
for(int i=hd[x];i;i=e[i].nxt) {
int y=e[i].to;
if(y==fa) continue;
dfs(y,x);
for(int j=m-1;j>=0;j--)
for(int k=1;k<=j;k++)
f[x][j]=max(f[x][j],f[x][j-k]+f[y][k]);
}
for(int i=m;i>=1;i--) f[x][i]=f[x][i-1]+d[x];//必须选自己,所以先求出m-1个物品的,再加上自己的
f[x][0]=0;
}
inline bool check(double mid) {
memset(f,0xcf,sizeof(f));
for(int i=1;i<=n;i++) d[i]=(double)a[i]-mid*b[i];
dfs(1,0);
for(int i=1;i<=n;i++)
if(f[i][m]>-eps) return 1;
return 0;
}
int main() {
n=read();m=read();
m=n-m;
for(int i=1;i<=n;i++) a[i]=read();
for(int i=1;i<=n;i++) b[i]=read();
for(int i=1,x,y;i<n;i++) {
x=read();y=read();
add(x,y);add(y,x);
}
double l=0,r=1000000,mid;
while(r-l>eps) {
mid=(l+r)/2.0;
if(check(mid)) l=mid;
else r=mid;
}
printf("%.1lf",l);
return 0;
}
https://www.luogu.com.cn/problem/P1730
Poj3155——Hard Life最大密度子图
Zoj2676——Network Wars
游戏——最大密度子图变种
Poj3266——CowSchool0/1分数规划+数据结构
【P6087,JSOI2015】送礼物
【BZOJ3232】圈地游戏(难)