01分数规划小记

update:

  • 2024.7.10 补两道 分数规划 + 网络流的好题

01 分数规划

求形如 $$\frac{\sum_{i=1}^{n}{a_i \times c_i}}{\sum_{i=1}^{n} {b_i \times c_i}},c_i \in {0,1}$$ 式子的最大值或最小值。

一般方法有两个:二分法与迭代法。这里不再赘述。
01分数规划时常与图论结合。

例题:

I P1570 KC 喝咖啡

\(\large\lambda =\frac{\sum_{i=1}^{n}{a_i}}{\sum_{i=1}^{n} {b_i}}\),则 \(\large\sum_{i=1}^{n} {a_i} - \lambda \times \sum_{i=1}^{n} {b_i} = 0\),再设 \(\large G(\lambda) = \sum_{i=1}^{n} {a_i} - \lambda \times \sum_{i=1}^{n} {b_i}\),则 \(G(\lambda)\) 就是一个斜率为负的一次函数,我们拆开得 \(G(\lambda) = \sum_{i=1}^{n} ({a_i} - \lambda \times {b_i})\),贪心选择 \(\large c_i = a_i - \lambda * b_i\) 即可。

二分法:

点击查看代码
#include <bits/stdc++.h> 
using namespace std;
const int N = 210;
const double eps = 1e-5;
//01分数规划 
int n,m;
double a[N],b[N],c[N];
int main(){
    scanf("%d%d",&n,&m);
    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 = 1010;
    while(l + eps < r){
    	double mid = (l + r) / 2.0;
    	for(int i = 1;i <= n;i++)c[i] = a[i] - mid * b[i];
    	sort(c+1,c+1+n);//排序找最大值
    	double s = 0;
    	for(int i = n;i > n-m;i--)s += c[i];
    	if(s >= 0)l = mid;
    	else r = mid;
	}
	printf("%.3lf\n",r);
    
    return 0;
    
}

II P4377 [USACO18OPEN] Talent Show G

首先要求 \(\Large\frac{\sum_{i=1}^{n}{t_i}}{\sum_{i=1}^{n} {w_i}}\) 最大值,但是有一些限制,只有选出来的 \(w_i\) 之和大于 \(W\) 才可以。
我们把每个牛当做 质量为 \(w_i\),价值为 \(t_i - \lambda \times w_i\),那么我们就可以想到背包,设 \(f_i\) 为重量之和为 \(i\) 的物品最大价值,判断 \(f[W]\) 是否大于 \(0\),直接转移即可。

点击查看代码
#include <bits/stdc++.h> 
using namespace std;
//01分数规划 + 背包 
#define ll long long
const int N = 300,M = 1010;
const double eps = 1e-5,inf = 1e8;
int n,W;
double w[N],t[N],f[M];
bool check(double mid){
	for(int i = 1;i <= 1000;i++)f[i] = -inf;
	for(int i = 1;i <= n;i++){
		for(int j = W;j >= 0;j--){
			int k = min(W,j+(int)w[i]);//超过的直接放到f[W]上 
			f[k] = max(f[k],f[j] + t[i] - mid * w[i]);
		} 
	}
	return f[W] >= 0;
}
int main(){
    scanf("%d%d",&n,&W);
    for(int i = 1;i <= n;i++)scanf("%lf%lf",&w[i],&t[i]);
    double l = 0,r = 1e6;
    while(l + eps < r){
    	double mid = (l + r) / 2;
    	if(check(mid))l = mid;
    	else r = mid;
	}
	printf("%d\n",int(r*1000));
    
    return 0;
    
}


III P2868 [USACO07DEC] Sightseeing Cows G

一个图,有点权有边权,设一个环内点集为 \(S\),边集为 \(E\)求最大值 \(\Large\frac{\sum_{u \in S}{A_u}}{\sum_{e \in E}{T_e}}\),很显然的01分数规划模型,只是要求选出来的点组成一个环即可,二分后,我们要找是否存在一个环使 \(G(mid)\) (别忘了 \(c_i = a_i - \lambda \times b_i\))大于 \(0\),这是什么?
最长路判正环!!(不会的去学-_-)(本题 \(spfa\) 判正环即可)
然后正常二分即可。

点击查看代码
#include <bits/stdc++.h> 
using namespace std;
const int N = 2e3+10,M = 1e4+10;
const double eps = 1e-4,inf = 1e8;
//01分数规划 + 判负环 
int n,m;
struct made{
    int nx,ver;
    double ed;
}e[M<<1];
int hd[N],tot;
double a[N];
void add(int x,int y,double z){
    tot++;
    e[tot].nx = hd[x],e[tot].ver = y,e[tot].ed = z,hd[x] = tot;
}
double d[N];
int in[N];bool v[N];
bool spfa(double mid){
    memset(v,0,sizeof v);
    memset(in,0,sizeof in);
    for(int i = 0;i <= n;i++)d[i] = -inf;
    queue<int>q;q.push(0),v[0] = 1,d[0] = 0;
    while(!q.empty()){
        int x = q.front();q.pop();
        v[x] = 0;
        for(int i = hd[x];i;i = e[i].nx){
            int y = e[i].ver;double z = e[i].ed;
            z = a[x] - mid * z;
            if(d[y] < d[x] + z){
                d[y] = d[x] + z;
                in[y]++;
                if(in[y] > n)return 1;//判正环
                if(!v[y])v[y] = 1,q.push(y);
            }
        }
    }
    return 0;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)scanf("%lf",&a[i]);
    for(int i = 1;i <= n;i++)add(0,i,0);
    for(int i = 1;i <= m;i++){
        int x,y;double z;
        scanf("%d%d%lf",&x,&y,&z);
        add(x,y,z);
    }
    double l = 0,r = 1e5;
    while(l + eps < r){
        double mid = (l + r) / 2;
        if(spfa(mid))l = mid;
        else r = mid;
    }
    printf("%.2lf\n",r);
    
    return 0;
    
}

IV P3199 [HNOI2009] 最小圈

我们要找图中一个环使得环上权值的平均值最小,即一个环的边集 \(E\),使 \(\Large\frac{\sum_{e \in E}{T_i}}{\sum{b_i}}\) 最小,这里的 \(b_i\) 都等于 \(1\),那么使 \(c_i = T_i - \lambda\),二分,判断 \(G(mid)\) 最小值是否小于 \(0\) 即可,即判断图中是否有负环
做完了?没有,如果你直接写 \(spfa\) 判负环就会得到 \(50\) 分,因为 \(spfa\) 复杂度是 \(O(km)\),会爆,所以我们就要开摆原神启动
有一个 \(dfs\) 实现 \(spfa\) 判负环的科技,可以快速判负环,但是会算不了最短路,所以慎用
时间复杂度不会,反正就是很快 (:з」∠),咕咕咕。

点击查看代码
#include <bits/stdc++.h> 
using namespace std;
const int N = 3e3+10,M = 2e4+10;
const double eps = 1e-10,inf = 1e8;
//01分数规划 + 判负环 
int n,m;
struct made{
    int nx,ver;
    double ed;
}e[M<<1];
int hd[N],tot;
void add(int x,int y,double z){
    tot++;
    e[tot].nx = hd[x],e[tot].ver = y,e[tot].ed = z,hd[x] = tot;
}
double d[N];
bool v[N];
bool spfa(int x,double mid){
	v[x] = 1;
    for(int i = hd[x];i;i = e[i].nx){
    	int y = e[i].ver;double z = e[i].ed;
    	z -= mid;
    	if(d[y] > d[x] + z){
    		d[y] = d[x] + z;
    		if(v[y] || spfa(y,mid))return 1;//被遍历过了即有负环
		}
	}
	v[x] = 0;
	return 0;
}//dfs-spfa判负环 
bool check(double mid){
    memset(v,0,sizeof v);
    for(int i = 1;i <= n;i++)d[i] = 0;//初值为0
	for(int i = 1;i <= n;i++)
	    if(spfa(i,mid))return 1;
	return 0;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)add(0,i,0);
    for(int i = 1;i <= m;i++){
        int x,y;double z;
        scanf("%d%d%lf",&x,&y,&z);
        add(x,y,z);
    }
    double l = -1e7,r = 1e7;
    while(l + eps < r){
        double mid = (l + r) / 2;
        if(check(mid))r = mid;
        else l = mid;
    }
    printf("%.8lf\n",r);
    
    return 0;
    
}

V 3999. 制作人偶

首先二分 \(mid\),转化为求 \(\dfrac{\sum v_i}{\sum w_i} = \lambda\) 最大值,我们设 \(S = \sum v_i\),则转化为求 \(\dfrac{S - \sum v_i}{\sum w_i} = \lambda\) 的最小值,及判断是否存在 \(\sum v_i - mid \times \sum w_i \geq 0\)

我们考虑建立最小割模型,则我们设源点 \(s\)不选该点,汇点为选了该点

则对于每个点 \(i\),我们建 \((i,t,mid\times w_i)\)
对于每条边 \((x,y,z)\),我们建 \((s,x,\frac z 2),(s,y,\frac z 2),(x,y,\frac z 2),(y,x,\frac z 2)\) 这些边,然后跑网络流,判断是否有 \(S - maxflow \geq 0\) 即可。

注意double类型网络流中需注意精度问题

代码

VI 2047. [ZOJ 2676]网络战争

首先题目条件是简单的,选择一些边,需要保证 \(1\) 节点到 \(n\) 节点中的任意路线都经过至少一个边,即找到以 \(1\) 为源点,\(n\) 为汇点的图的割边

我们要求 \(\dfrac {\sum c}{k} = \lambda\)最小值,我们二分 \(mid\),则我们只判断是否满足 \(\sum c - k \times mid < 0\),且满足割边的条件,即求以 \(c - mid\) 为边权的最小割(若 \(c - mid \leq 0\),我们可以贪心直接累加上答案,不用建边)。

输出的是满足答案最小的方案数,即求最小割的方案数,我们只需在图上 \(dfs\) 出以源点的集合,把源点与汇点的集合交汇的边存起来即可。

代码

posted @ 2024-01-19 11:09  oXUo  阅读(25)  评论(0编辑  收藏  举报
网站统计