P4377 学习笔记

省流:经典的01分数规划+背包 DP。

题目传送门

话说分数规划这玩意我也不是很会。

这类分数规划+背包 DP 还是挺少见的,毕竟在 Luogu 上都只有两道,还有一道是 P4322,到时候有空也做一下。

下面讲一下分数规划是啥以及可以解决什么问题

这里主要讲解 01 分数规划。

01分数规划是一类经典的数学优化问题,它的核心是:在一群物品(每个物品有收益 \(a_i\) 和成本 \(b_i\))中,选出一个子集 \(S\),使得单位成本下的收益最大。(每个物体可以选或不选,即 01 分数规划)

用数学语言来说,就是求下列表达式的最大值:

\[\frac{\sum_{i \in S} a_i}{\sum_{i \in S} b_i} \]

oi-wiki 上对于“选子集”的操作是这么讲的:

给定一组 \(a_i\)\(b_i\),求一组 \(w_i \in \{0,1\}\),最小化或最大化下式的值:

\[\frac{\sum_{i=1}^n a_i \cdot w_i}{\sum_{i=1}^n b_i \cdot w_i} \]

这种解释更加体现了“01”二字。

那么就有一个新的问题:怎么求呢?

首先很容易想到贪心,即按照性价比 \(\dfrac{a_i}{b_i}\) 进行排序,但是这是错误的

举一个反例:

\[\begin{cases} a_1=5,b_1=3 \Rightarrow \frac{a_1}{b_1} \approx 1.67\\ a_2=4,b_2=2 \Rightarrow \frac{a_2}{b_2}=2\\ a_3=3,b_3=2 \Rightarrow \frac{a_3}{b_3} \approx 1.5\\ \end{cases} \]

按照贪心是选物品一和物品二,此时原式的值为 \(1.8\),但是如果选择物品二和物品三,原式的值为 \(1.75<1.8\),立刻否认了贪心。

最通用解决 01 分数规划的方法是二分

设当前二分到的答案为 \(x\),则问题转化为是否存在一个子集 \(S\),使得

\[\frac{\sum_{i \in S} a_i}{\sum_{i \in S} b_i} \ge x \]

开始变形!!

去分母并把 \(x\) 移到左边:

\[\sum_{i \in S} a_i - \sum_{i \in S} b_i \cdot x \ge 0 \]

合并两个 \(\sum\)

\[\sum_{i \in S} (a_i - b_i \cdot x) \ge 0 \]

那么此时每个物品的权值都变成了 \(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;
}
posted @ 2026-02-12 10:31  constexpr_ll  阅读(7)  评论(0)    收藏  举报