分数规划 基本题型&模板
分数规划 基本题型&模板
定义
01分数规划是这样的一类问题,有一堆物品,每一个物品有一个收益\(ai\),一个代价\(bi\),我们要求一个方案,选出k个物品,使选择的\(\sum{ai}/\sum{bi}\)最大。
基本做法是采用二分法,假设当前二分到的答案为x,那么:
\(\sum{ai}/\sum{bi}>=x\)可以转化为:\(\sum{(ai-x*b_i)}>=0\),所以每次check的时候将\(a_i-x*b_i\)进行排序,取前k个,判断他们的和是否大于等于0即可
模板
poj2976
给出两个数组a和b,\(a_i\)代表这门课获得的成绩,\(b_i\)代表这门课的满分是多少,现在要求你舍弃掉k门课,使得平均绩点(\(100*\sum{ai}/\sum{bi}\))最高。
直接二分即可,注意是舍弃k门课,所以相当于选择n-k门课,另外输出需要四舍五入,可以直接利用.0lf进行四舍五入
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <iostream>
#include <limits>
#include <map>
#include <set>
#include <vector>
using namespace std;
const int N = 1e6 + 5;
typedef long long LL;
int k, n;
double a[N], b[N], c[N];
const double eps = 1e-8;
// 和0做比较
int sgn(double x) {
if (fabs(x) < eps) return 0; // =0
if (x < 0)
return -1; // < 0
else
return 1; // > 0
}
bool check(double mid) {
for (int i = 0; i < n; i++) c[i] = a[i] - mid * b[i];
sort(c, c + n);
double sum = 0;
for (int i = n - 1; i >= k; i--) sum += c[i]; //选n-k项
return (sgn(sum) >= 0);
}
int main() {
while (scanf("%d%d",&n,&k)) {
if ((n + k) == 0) break;
for (int i = 0; i < n; i++) scanf("%lf", &a[i]);
for (int i = 0; i < n; i++) scanf("%lf", &b[i]);
double l = 0, r = 1; //成绩最大是1,也就是满分
while (r - l > 1e-8) { // r与l的间隔小于1e-8
double mid = (l + r) / 2; // 不需要考虑加一的事情
if (check(mid))
l = mid;
else
r = mid; // r和l都是mid}
}
printf("%.0lf\n",l*100);//四舍五入
}
return 0;
}
例题
最优比率生成环
acwing361观光奶牛
题意: 给定一张L个点、P条边的有向图,每个点都有一个权值f[i],每条边都有一个权值t[i]。求图中的一个环,使“环上各点的权值之和”除以“环上各边的权值之和”最大。输出这个最大值。 点数N~1e3, 边数M~5e3
题解: 二分枚举答案,然后根据这个mid来重新构图,与\(a_i-mid*b_i\)类似,本题为$ f[t]-midw[i]\(的和是否大于0,那么将每个点的权值下放到边上,然后判断是否存在正环即可,也可以变个符号,判断是否存在负环:具体操作就是spfa时当对t点的所有出边进行更新的时候,原来的边权w[i],变为\)midw[i] - f[t]$
#include <bits/stdc++.h>
using namespace std;
int const N = 1e3 + 10, M = 5e5 + 10;
double const eps = 1e-8;
int e[M], ne[M], w[M], idx, h[N], n, m, cnt[N], st[N], f[N];
double dist[N];
// 建邻接表
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
// spfa求负环(正环)
bool spfa(double mid) {
queue<int> q;
memset(dist, 0x3f, sizeof dist);
memset(cnt, 0, sizeof cnt);
memset(st, 0, sizeof st);
for (int i = 1; i <= n; i ++ ) {
st[i] = true;
q.push(i);
}
// dist[0] = 0, st[0] = 1, q.push(0); 如果希望能够正确求出dis
while (q.size()) {
int t = q.front(); // 取队首
q.pop(); // 出队首
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[t] + w[i] * mid - f[t]) { // 这里是判断负环,如果是判正环:1.初始化写成memset(dis, 0xc0, sizeof dis), 2.更新条件写成dist[j] < dist[t] + w[i]
dist[j] = dist[t] + w[i] * mid - f[t]; // 边权发生改变
cnt[j] = cnt[t] + 1; // 更新边数
if (cnt[j] >= n) return true; // 如果j点到源点的边数大于等于n
if (!st[j]) {
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main() {
cin >> n >> m;
memset(h, -1, sizeof h);
for (int i = 1; i <= n; ++i) scanf("%d", &f[i]);
for (int i = 1, a, b, c; i <= m; ++i) {
scanf("%d %d %d", &a, &b, &c);
add(a, b, c);
}
double l = 0, r = 1e9;
while (r - l > eps) {
double mid = (l + r) / 2;
if (spfa(mid)) l = mid;
else r = mid;
}
printf("%.2lf", l);
return 0;
}
最优比率生成树
acwing348 沙漠之王
题意: 大卫希望渠道的总成本和总长度的比值能够达到最小。他只希望建立必要的渠道,为所有的村庄提供水资源,这意味着每个村庄都有且仅有一条路径连接至首都。他的工程师对所有村庄的地理位置和高度都做了调查,发现所有渠道必须直接在两个村庄之间水平建造。由于任意两个村庄的高度均不同,所以每个渠道都需要安装一个垂直的升降机,从而使得水能够上升或下降。建设渠道的成本只跟升降机的高度有关,换句话说只和渠道连接的两个村庄的高度差有关。需注意,所有村庄(包括首都)的高度都不同,不同渠道之间不能共享升降机。
题解:
要求找出一棵\((a1/b1) + (a2/b2) +... + (an/bn)\)之和最大生成树
我们设\((a1/b1) + (a2/b2) +... + (an/bn) = mid\)。那么对应于每一条边我们可以得到一条新边\(ai-mid * bi\),采用二分的方式枚举mid,如果得到的使用新边\(ai-mid * bi\)建成的最小生成树的权值之和为0,那么这个mid就是我们的答案;否则,找其他的mid
#include <bits/stdc++.h>
using namespace std;
int const N = 2e3 + 10;
int n;
double dis[N], d[N][N], h[N][N]; // dis记录到最小生成树的最小距离,d数组记录两个点的最小距离,h数组记录两个点的最小高度
double x[N], y[N], z[N]; // 记录每个点输入的位置
bool vis[N]; // 判断每个点是否在最小生成树内
// prime算法查找新边建立的最小生成树是否满足条件
bool prime (double mid) {
memset(dis, 0x3f, sizeof dis);
memset(vis, 0, sizeof vis);
double sum = 0; // 最小生成树的边权值和
vis[1] = 1;
// 把1号点放入集合后,计算和1号点相邻的所有点的距离
for (int i = 2; i <= n; ++i)
dis[i] = h[1][i] - mid * d[1][i];
for (int i = 2; i <= n; ++i) {
// 找出到最小生成树距离最小的那个点
double mini = 0x3f3f3f3f;
int u = -1;;
for (int j = 2; j <= n; ++j) {
if (!vis[j] && dis[j] < mini) {
mini = dis[j], u = j;
}
}
// 放入最小生成树内
vis[u] = 1;
sum += dis[u];
// 更新所有与u点相邻的点
for (int j = 2; j <= n; ++j) {
if (!vis[j] && dis[j] > h[u][j] - mid * d[u][j])
dis[j] = h[u][j] - mid * d[u][j]; // 用广义边更新
}
}
// 判断是否满足条件
if (sum >= 0) return false; // mid取太小
else return true;
}
int main() {
while (scanf("%d", &n) != EOF && n) {
memset(x, 0, sizeof x);
memset(y, 0, sizeof y);
memset(z, 0, sizeof z);
memset(d, 0, sizeof d);
memset(h, 0, sizeof h);
for (int i = 1; i <= n; ++i) {
cin >> x[i] >> y[i] >> z[i];
// 建立一张完全图
for (int j = 1; j < i; ++j) {
d[i][j] = d[j][i] = sqrt((x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j])); // 记录i点和前j个点的距离
h[i][j] = h[j][i] = fabs(z[i] - z[j]); // 记录高度差
}
}
// 01分数规划,二分查找答案
double l = 0, r = 1000.0, mid;
while (r - l > 1e-6) {
mid = (l + r) / 2;
if (prime(mid)) r = mid; // 完全图采用prime算法
else l = mid;
}
printf("%.3f\n", mid);
}
return 0;
}
树形背包01分数规划
P1642 规划
某地方有N个工厂,有N-1条路连接它们,且它们两两都可达。每个工厂都有一个产量值和一个污染值。现在工厂要进行规划,拆除其中的M个工厂,使得剩下的工厂依然连成一片且 总产量/总污染 的值最大。
还是分数规划的基本套路,二分mid,然后去跑树形背包即可
#include <bits/stdc++.h>
using namespace std;
const int N = 1e2 + 5;
typedef long long LL;
int n, m, a[N], b[N];
vector<int> mp[N];
double dp[N][N];
const double eps = 1e-8;
// 和0做比较
int sgn(double x) {
if (fabs(x) < eps) return 0; // =0
if (x < 0)
return -1; // < 0
else
return 1; // > 0
}
int sz[N];
void dfs(int now, int fa, double mid) {
dp[now][0] = 0;
sz[now] = 1;
for (int i = 0; i < mp[now].size(); i++) {
int son = mp[now][i];
if (son == fa) continue;
dfs(son, now, mid);
sz[now] += sz[son];
for (int j = min(m, sz[now]) - 1; j >= 0; j--)
for (int k = 0; k <= min(j, sz[son]); ++k)
dp[now][j] = max(dp[now][j], dp[now][j - k] + dp[son][k]);
}
for (int i = min(m, sz[now]); i >= 1; --i)
dp[now][i] = dp[now][i - 1] + 1.0 * a[now] - mid * b[now];
}
bool check(double mid) {
for (int i = 0; i <= n;i++){
for (int j = 0; j <= m; j++) dp[i][j] = -1e18;
}
dfs(1, 0, mid);
for (int i = 1; i <= n; i++)
if (sgn(dp[i][m]) >= 0) return true;
return false;
}
int main() {
cin >> n >> m;
m = n - m; //拆除m个,也就是选择n-m个
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i = 1; i <= n; i++) cin >> b[i];
for (int i = 0; i < n - 1; i++) {
int x, y;
cin >> x >> y;
mp[x].push_back(y), mp[y].push_back(x);
}
double l = 0, r = 100000;
while (r - l > 1e-8) { // r与l的间隔小于1e-8
double mid = (l + r) / 2; // 不需要考虑加一的事情
if (check(mid))
l = mid;
else
r = mid;
}
printf("%.1lf\n", l);
return 0;
}