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\) 出以源点的集合,把源点与汇点的集合交汇的边存起来即可。