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】圈地游戏(难)

https://www.luogu.com.cn/problem/P4322

posted @ 2020-08-22 09:32  ke_xin  阅读(39)  评论(0编辑  收藏  举报