P4377 学习笔记
省流:经典的01分数规划+背包 DP。
话说分数规划这玩意我也不是很会。
这类分数规划+背包 DP 还是挺少见的,毕竟在 Luogu 上都只有两道,还有一道是 P4322,到时候有空也做一下。
下面讲一下分数规划是啥以及可以解决什么问题
这里主要讲解 01 分数规划。
01分数规划是一类经典的数学优化问题,它的核心是:在一群物品(每个物品有收益 \(a_i\) 和成本 \(b_i\))中,选出一个子集 \(S\),使得单位成本下的收益最大。(每个物体可以选或不选,即 01 分数规划)
用数学语言来说,就是求下列表达式的最大值:
oi-wiki 上对于“选子集”的操作是这么讲的:
给定一组 \(a_i\) 和 \(b_i\),求一组 \(w_i \in \{0,1\}\),最小化或最大化下式的值:
这种解释更加体现了“01”二字。
那么就有一个新的问题:怎么求呢?
首先很容易想到贪心,即按照性价比 \(\dfrac{a_i}{b_i}\) 进行排序,但是这是错误的。
举一个反例:
按照贪心是选物品一和物品二,此时原式的值为 \(1.8\),但是如果选择物品二和物品三,原式的值为 \(1.75<1.8\),立刻否认了贪心。
最通用解决 01 分数规划的方法是二分。
设当前二分到的答案为 \(x\),则问题转化为是否存在一个子集 \(S\),使得
开始变形!!
去分母并把 \(x\) 移到左边:
合并两个 \(\sum\);
那么此时每个物品的权值都变成了 \(a_i - b_i \cdot x\),于是问题转化为:
求一个子集 \(S\),使得权值和 \(\ge 0\)。
二分即可。
回归我们要解决的问题。
首先分析题意。
我们有 \(n\) 头奶牛,第 \(i\) 头有重量 \(w_i\) 和才艺 \(t_i\),我们要求选出一个子集 \(S\),使得:
-
总重量不低于下限 \(W\);
-
最大化比值 \(R=\dfrac{\sum_{i \in S} t_i}{\sum_{i \in S} w_i}\)
问题符合经典 01 分数规划的模样,但是多了一个下限 \(W\)。
上面分析过,贪心肯定是不行的,我们需要二分法。
考虑在有下限 \(W\) 时,check 函数如何写。
考虑 01 背包,如果背包过程中总重量超过 \(W\),直接视为 \(W\) 即可。
code
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <climits>
#define I using
#define AK namespace
#define IOI std
#define A return
#define C 0
#define Ofile(s) freopen(s".in", "r", stdin), freopen (s".out", "w", stdout)
#define Cfile(s) fclose(stdin), fclose(stdout)
#define fast ios::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL);
I AK IOI;
using ll = long long;
using uint = unsigned int;
using ull = unsigned long long;
using lb = long double;
using pii = pair<int, int>;
using pll = pair<ll, ll>;
using pil = pair<int, ll>;
using pli = pair<ll, int>;
constexpr int mod = 998244353;
constexpr int maxn = 2.5e2 + 5;
constexpr int maxk = 1e3 + 5;
constexpr double eps = 1e-6;
int n, W;
double l, r;
int w[maxn], t[maxn];
double dp[maxk];
bool check(double mid) {
for (int i = 1; i <= W; i++)
dp[i] = -1e9; // 赋一个极小值
for (int i = 1; i <= n; i++)
for (int j = W; j >= 0; j--) { // 01 背包是倒序
int k = min(W, j + w[i]); // 超过 W 看成 W 即可。
dp[k] = max(dp[k], dp[j] + t[i] - mid * w[i]); // 01 背包常规操作
}
return dp[W] >= 0;
}
int main() {
freopen("std.in", "r", stdin);
freopen("std.out", "w", stdout);
fast;
cin >> n >> W;
for (int i = 1; i <= n; ++i)
cin >> w[i] >> t[i], r += t[i]; // 右边界小优化,实在不行你定义一个超大的数也行
while (l + eps <= r) { // 实数二分加一个极小的 eps 控精度
double mid = (l + r) / 2;
if (check(mid))
l = mid;
else
r = mid;
}
cout << (int) (l * 1000); // 别忘了输出时要乘 1000 向下取整
A C;
}

浙公网安备 33010602011771号