01分数规划
Contents
介绍
01分数规划用于 求一个分式的极值
例如: 给定 $a_i, b_i$,选出一组$i$, 使得 $\frac{\sum a_i}{\sum b_i}$ 最大/最小?
方法 (二分)
一般使用二分答案的方法进行求解,假设我们要求最大值,那么 二分一个答案 $mid$,有
$\frac{\sum a_i}{\sum b_i} \geq mid$, 所以
$\sum a_i- (mid \times b_i) \geq 0$
所以只要求出左边的最大值,判断是否 >= 0 即可!
- 如果左边最大值 >= 0,说明当前答案 $mid$ 可行,提高下边界
- 如果左边最大值 < 0,说明当前答案 $mid$ 不可行,降低上边界
例题
例1 POJ2976 Dropping tests
题意
有 n 个物品,每个物品 $i$ 有两个权值 $a_i, b_i$。
选 $k$ 个物品 ,使得 $\frac{\sum a_i}{\sum b_i}$ 最大。
题解
二分答案,当前答案为 $mid$ 时,把第 $i$ 个物品的权值设为 $a_i - mid \times b_i$,然后取最大的 $k$ 个即可得到最大值。
例2 洛谷P1419
题意
给定 $n$ 个整数,求一个长度在 $[S,T]$ 之间的subarray(连续),使得平均值最大?
题解
二分答案,当前答案为 $mid$ 时,我们遍历一下区间的右端点 $r$,从 $1$ 遍历到 $n$,固定一个 $r$,则我们需要找到 左端点 $l$ 使得 $\frac{\sum\limits_{i=l}^ra_i}{r-l+1}$ 最大。
令 $\frac{\sum\limits_{i=l}^ra_i}{r-l+1} \geq mid$,有 $\sum\limits_{i=l}^r(a_i - mid) \geq 0$,
定义一个新的数组$b$,其中 $b_i = a_i - mid$。
左边的最大值就是 $b$ 这个数组中最大的连续区间,用前缀和即可。
另外一种思考方法:
二分答案,当前答案为 $mid$ 时,我们遍历一下区间的右端点 $r$,从 $1$ 遍历到 $n$,固定一个 $r$,则我们需要找到 左端点 $l$ 使得 $\frac{sum[r] - sum[l-1]}{r-l+1}$ 最大。
$\frac{sum[r] - sum[l-1]}{r-l+1} \geq mid$ ,有 $(mid \times (l-1) - sum[l-1]) - (mid \times r - sum[r]) \geq 0$
因为 $r$ 固定,所以只要找 $mid*l - sum[l]$ 的最大值,其中 $l \in [\max(0, r-T), r-s]$。
遍历 $r$ 的时候,维护一个单调队列即可。
luogu-P1419-AC代码
#include <bits/stdc++.h>
using namespace std;
const double eps = (double)1e-6;
const int maxn = 1e5+5;
int n, s, t;
int arr[maxn];
int sum[maxn];
double a[maxn];
int q[maxn];
int head = 0, tail = -1;
double ans = -1e4;
bool check(double cur) {
for (int i = 0; i <= n; i++) a[i] = cur * (double)(i) - (double)(sum[i]);
double res = -1e18;
head = 0, tail = -1;
for (int r = s; r <= n; r++) {
while (head <= tail && q[head] < r-t) head++;
while (head <= tail && a[q[tail]] < a[r-s]) tail--;
q[++tail] = r-s;
res = max(res, a[q[head]] - a[r]);
}
// for (int r = 1; r <= n; r++) {
// double tmp = -1e18;
// for (int i = max(0, r-t); i <= r-s; i++) {
// tmp = max(tmp, a[i]);
// }
// res = max(res, tmp - a[r]);
// }
if (res >= 0) return 1;
return 0;
}
int main() {
scanf("%d%d%d",&n,&s,&t);
for (int i = 1; i <= n; i++) {
scanf("%d", &arr[i]);
sum[i] = sum[i-1] + arr[i];
}
double l = -1e4, r = 1e4;
while (abs(l-r) > eps) {
double mid = (l+r) * 0.5;
if (check(mid)) {
l = mid;
ans = mid;
} else {
r = mid;
}
}
printf("%.3f\n", ans);
}
例3 洛谷P4377
题意
有 n 个物品,每个物品 $i$ 有两个权值 $a_i, b_i$。
你可以选 $k$ 个物品 ,使得 $\frac{\sum a_i}{\sum b_i}$ 最大, 且 $\sum b_i \geq W$
题解
和例1几乎一样,但是多了一个 $\sum b_i \geq W$ 的限制。
设 $d_i = a_i - mid \times b_i$,然后我们要做的事就是:
选取一组$i$,保证在 $\sum b_i \geq W$ 的前提下,使得 $\sum d_i$ 最大
可以用01背包解决,令 $dp[n][k]$ 为:使用前 $n$ 个元素,$\sum b_i = k$ 时, $\sum d_i$ 的最大值。
那么答案就是 $dp[n][W]$。
如果在状态转移过程中出现 $k > W$,直接转移到 $k = W$ 的状态上即可。
注: 写01背包的时候不要忘了 倒序枚举 !
luogu-P4377-AC代码
using namespace std;
#include <bits/stdc++.h>
const double eps = (double)1e-6;
const int maxn = 255;
int n,W;
int w[maxn], t[maxn];
double dp[1001];
struct node {
double d;
int w;
} arr[maxn];
bool check(double cur) {
for (int i = 1; i <= n; i++) {
arr[i] = {(double)t[i] - cur * (double)w[i], w[i]};
}
fill(dp, dp+1001, -1e9);
dp[0] = 0.0;
for (int i = 1; i <= n; i++) {
for (int d = 1000; d >= 0; d--) {
int tar = min(d + arr[i].w, 1000);
dp[tar] = max(dp[tar], dp[d] + arr[i].d);
}
}
for (int d = W; d <= 1000; d++) {
if (dp[d] >= 0.0) return 1;
}
return 0;
}
int main() {
scanf("%d%d", &n,&W);
for (int i = 1; i <= n; i++) {
scanf("%d%d", &w[i], &t[i]);
}
double l = 0.0, r = 1e6, ans = 0.0;
while (abs(l-r) >= eps) {
double mid = (l+r) * 0.5;
if (check(mid)) {
l = mid;
ans = max(ans, mid);
} else {
r = mid;
}
}
printf("%d\n", (int)((ans+5e-5) * 1000));
}
例4 洛谷P4322
题意
给定一棵$N$个节点的树,每个节点 $i$ 具有两个权值 $P_i, S_i$,求树中的一组节点,满足:
- $\frac{\sum\limits_i P_i}{\sum\limits_i S_i}$ 最大
- 如果 节点 $i$ 被选中了,那么它的parent $R_i$ 也必须被选中
- 选中的节点数量 刚好为$K$
求满足条件的最大比值?
题解
首先看条件1:$\frac{\sum\limits_i P_i}{\sum\limits_i S_i}$ 最大,仍然是分数规划的套路,二分答案。令 $d_i = P_i - mid \times S_i$
所以问题转化为求一组节点使得 $\sum d_i$ 最大。
然后就会发现,这是一个经典的树形dp模型,和 选课(洛谷P2014) 几乎一样。
具体做法和注意事项见下一篇博客。