01 分数规划
01 分数规划指这样一类问题,对于每个元素,有 \(a\) 和 \(b\) 两种属性,要求按规则选出一些物品后 \(\dfrac{\sum a}{\sum b}\) 最大。
这样的问题可以二分答案 \(x\),看 \(\dfrac{\sum a}{\sum b}\) 是否可能 \(\ge x\),二分上界可以设成单个元素 \(\dfrac{a}{b}\) 的最大值。
化简不等式会得到 \(\sum a - x \sum b \ge 0\),也就是 \(\sum (a - x \cdot b) \ge 0\)。
这样一来,问题就转化为在该规则下选出的 \(a - x \cdot b\) 之和的最大值是否 \(\ge 0\)。
例题:P1404 平均数
分析:可以用 01 分数规划的思想看待这个问题,这里认为每个元素的 \(a\) 属性就是这个数的大小,\(b\) 属性就是数量,也就是 \(1\),希望 \(\dfrac{\sum a}{\sum b}\) 尽可能大。
二分答案后转化为选出的 \(a - x \cdot b\) 之和大于等于 \(0\),也就是 \(a - x\) 之和大于等于 \(0\),就是把所有元素减 \(x\) 以后让选出的数之和大于等于 \(0\)。
注意精度问题,可以在刚开始把每个数都乘以 \(1000\),然后进行二分,整体时间复杂度为 \(O(n \log a)\)。
参考代码
#include <cstdio>
#include <algorithm>
using std::max;
using std::min;
using ll = long long;
const int N = 100005;
int a[N], n, m;
ll sum[N], pre[N];
bool check(int x) {
for (int i = 1; i <= n; i++) {
sum[i] = sum[i - 1] + a[i] - x;
pre[i] = min(pre[i - 1], sum[i]);
}
for (int i = m; i <= n; i++)
if (sum[i] - pre[i - m] >= 0) return true;
return false;
}
int main()
{
scanf("%d%d", &n, &m);
int l = 0, r = 0, ans = 0;
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]); a[i] *= 1000;
r = max(r, a[i]);
}
while (l <= r) {
int mid = (l + r) / 2;
if (check(mid)) {
l = mid + 1; ans = mid;
} else {
r = mid - 1;
}
}
printf("%d\n", ans);
return 0;
}
习题:P10450 [USACO03MAR] Best Cow Fences G
解题思路
同 P1404 平均数。
参考代码
#include <cstdio>
#include <algorithm>
using namespace std;
using ll = long long;
const int N = 1e5 + 5;
int n, l, a[N];
// s: 前缀和数组, mins: s[i]的前缀最小值数组
ll s[N], mins[N];
// 检查是否存在一个长度不小于l的子段,其(放大1000倍后的)平均值大于等于avg
bool check(int avg) {
s[0] = 0; mins[0] = 0;
// 步骤1: 计算新序列 b_i = a_i - avg 的前缀和s,并同时计算s的前缀最小值mins
for (int i = 1; i <= n; i++) {
// s[i] 存储 b[1]...b[i] 的和
s[i] = s[i - 1] + (a[i] - avg);
// mins[i] 存储 s[0]...s[i] 中的最小值
mins[i] = min(mins[i - 1], s[i]);
}
// 步骤2: 检查是否存在满足条件的子段
// 条件是: s[i] - s[j] >= 0, 且 i - j >= l
// 等价于: s[i] >= s[j], 且 j <= i - l
// 为了让 s[i] >= s[j] 尽可能成立,需要最小的 s[j]
// mins[i-l] 正是 s[0]...s[i-l] 的最小值
for (int i = l; i <= n; i++) {
if (s[i] - mins[i - l] >= 0) return true;
}
return false;
}
int main()
{
scanf("%d%d", &n, &l);
int low = 0, high = 0;
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
// 整数二分技巧:将所有输入和计算都放大1000倍,避免浮点数精度问题
a[i] *= 1000;
// 更新二分上界
high = max(high, a[i]);
}
int ans = 0;
// 二分答案:寻找最大的可行的平均值(的1000倍)
while (low <= high) {
int mid = low + (high - low) / 2;
if (check(mid)) {
// 如果mid可行,说明答案可能更大,尝试右半部分
low = mid + 1;
ans = mid; // 记录可行的答案
} else {
// 如果mid不可行,说明答案在左半部分
high = mid - 1;
}
}
// 输出最终记录的可行解
printf("%d\n", ans);
return 0;
}
习题:P1419 寻找段落
解题思路
在上一题的基础上多了长度在 \([S, T]\) 的限制。用单调队列维护前缀和的区间最小值。
参考代码
#include <cstdio>
#include <deque>
using std::deque;
const int N = 100005;
const double INF = 1e4;
const double EPS = 1e-5;
int n, s, t, a[N];
double sum[N];
bool check(double x) {
for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + (a[i] - x);
deque<int> dq;
for (int i = s; i <= n; i++) {
while (!dq.empty() && dq.front() < i - t) dq.pop_front();
while (!dq.empty() && sum[dq.back()] > sum[i - s]) dq.pop_back();
dq.push_back(i - s);
if (sum[i] - sum[dq.front()] > -EPS) return true;
}
return false;
}
int main()
{
scanf("%d%d%d", &n, &s, &t);
double l = -INF, r = INF;
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
while (r - l > EPS) {
double mid = (l + r) / 2;
if (check(mid)) {
l = mid;
} else {
r = mid;
}
}
printf("%.3f\n", l);
return 0;
}

浙公网安备 33010602011771号