一、01分数规划
1.1 01分数规划
01分数规划用来求一个分式的极值。模型如下:
给出
a
i
a_i
ai 和
b
i
b_i
bi,求一组
w
i
∈
{
0
,
1
}
w_i \in \{ 0, 1 \}
wi∈{0,1}最小化或最大化
∑
i
=
1
n
a
i
×
w
i
∑
i
=
1
n
b
i
×
w
i
\frac{\sum_{i=1}^{n}a_i \times w_i}{\sum_{i=1}^{n}b_i \times w_i}
∑i=1nbi×wi∑i=1nai×wi
1.2 二分法
分数规划问题的通用方法是二分。
假设我们要求最大值。二分一个最大值M,那么有:
∑
a
i
×
w
i
∑
i
=
1
n
b
i
×
w
i
>
M
∑
a
i
×
w
i
−
M
×
∑
i
=
1
n
b
i
×
w
i
∑
w
i
×
(
a
i
−
M
b
i
)
\begin{align} & \frac{\sum_{}^{}a_i \times w_i}{\sum_{i=1}^{n}b_i \times w_i} \gt M \\ & \sum_{}^{}a_i \times w_i - M\times\sum_{i=1}^{n}b_i \times w_i \\ & \sum_{}^{}w_i\times (a_i - Mb_i) \end{align}
∑i=1nbi×wi∑ai×wi>M∑ai×wi−M×i=1∑nbi×wi∑wi×(ai−Mbi)
那么只要求出不等号左边的式子的最大值就行了。如果最大值比 0 要大,说明 mid 是可行的,否则不可行。
有时候01分数规划也会和网络流结合。详见最小割问题合集,最大权闭合图,最大密度子图,最小权点覆盖,最大权独立子图,OJ练习,代码详解-CSDN博客
1.3 题目练习
1.3.1 P10505 Dropping Test
原题链接
P10505 Dropping Test
思路分析
模板题,分式的最大值,最后 * 100 输出即可。
AC代码
#include <bits/stdc++.h>
using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned;
using u128 = unsigned __int128;
constexpr double eps = 1E-6;
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, k;
while (std::cin >> n >> k, n) {
std::vector<int> a(n), b(n);
for (int i = 0; i < n; ++ i) {
std::cin >> a[i];
}
for (int i = 0; i < n; ++ i) {
std::cin >> b[i];
}
auto check = [&](double m) -> bool{
std::vector<double> st;
for (int i = 0; i < n; ++ i) {
st.push_back(a[i] - b[i] * m);
}
std::ranges::sort(st);
double s = 0;
for (int i = k; i < n; ++ i) {
s += st[i];
}
return s >= 0;
};
double lo = 0, hi = 1;
while (lo + eps < hi) {
double x = (lo + hi) / 2;
if (check(x)) {
lo = x;
} else {
hi = x;
}
}
std::cout << std::round(lo * 100) << '\n';
}
return 0;
}
1.3.2 P4377 [USACO18OPEN] Talent Show G
原题链接
P4377 [USACO18OPEN] Talent Show G
思路分析
sum(t) / sum(w) >= m,m 为二分最大值
那么 sum(t - w * m) >= 0,我们如何得到sum(w) >= W下最大的sum(t - w * m)?——01背包
AC代码
#include <bits/stdc++.h>
using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned;
using u128 = unsigned __int128;
constexpr double eps = 1E-4;
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n, W;
std::cin >> n >> W;
std::vector<int> w(n), t(n);
for (int i = 0; i < n; ++ i){
std::cin >> w[i] >> t[i];
}
/*
sum(t) / sum(w) >= M
sum t - wM >= 0
*/
auto check = [&](double m) -> bool{
std::vector<double> f(W + 1, std::numeric_limits<double>::lowest() / 2);
f[0] = 0;
for (int i = 0; i < n; ++ i) {
for (int j = W; j >= 0; -- j) {
int nj = std::min(W, j + w[i]);
f[nj] = std::max(f[nj], f[j] + t[i] - w[i] * m);
}
}
return f[W] >= 0;
};
double lo = 0, hi = std::accumulate(t.begin(), t.end(), 0);
while (lo + eps <= hi) {
double x = (lo + hi) / 2;
if(check(x)) {
lo = x;
} else {
hi = x;
}
}
std::cout << (int)(1000 * lo) << '\n';
return 0;
}
1.3.3 Desert King
原题链接
Desert King
思路分析
01 分数规划 + 最小生成树
l(i, j) 为 节点 i 和 j 之间的欧氏距离,h(i, j) 为二者高度差
那么二分最大比率 m,有 sum(l) / sum(h) <= m,即 sum(l - m * h) <= 0
我们对于 节点i,j,以 l(i, j) - m * h(i, j) 为边权求最小生成树即可。
由于是稠密图,所以 直接用prim算法
AC代码
#include <iostream>
#include <vector>
#include <cmath>
#include <cassert>
#include <iomanip>
const double eps = 1E-5;
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(0);
std::cout.tie(0);
int n;
while (std::cin >> n, n) {
std::vector<int> x(n), y(n), z(n);
for (int i = 0; i < n; ++ i) {
std::cin >> x[i] >> y[i] >> z[i];
}
// sum(h) / sum(l) <= k
// sum(h - l * k)
std::vector<std::vector<double>> l(n, std::vector<double>(n));
std::vector<std::vector<int>> h(n, std::vector<int>(n));
for (int i = 0; i < n; ++ i) {
for (int j = 0; j < n; ++ j) {
l[i][j] = sqrt(1.0 * (x[i] - x[j]) * (x[i] - x[j]) + 1.0 * (y[i] - y[j]) * (y[i] - y[j]));
h[i][j] = abs(z[i] - z[j]);
}
}
double lo = 0, hi = 1000;
while (lo + eps <= hi) {
double m = (lo + hi) / 2;
double res = 0;
std::vector<bool> st(n);
std::vector<double> min(n);
for (int i = 0; i < n; ++ i) {
min[i] = h[0][i] - m * l[0][i];
}
min[0] = 0;
st[0] = true;
for (int k = 0; k < n - 1; ++ k) {
int mini = -1;
for (int i = 0; i < n; ++ i) {
if (!st[i] && (mini == -1 || min[mini] > min[i])) {
mini = i;
}
}
assert(mini != -1);
res += min[mini];
st[mini] = true;
for (int i = 0; i < n; ++ i) {
if (!st[i]) {
min[i] = std::min(min[i], h[mini][i] - m * l[mini][i]);
}
}
}
if (res <= eps) {
hi = m;
} else {
lo = m;
}
}
std::cout << std::fixed << std::setprecision(3) << hi << '\n';
}
return 0;
}
1.3.4 P3199 [HNOI2009] 最小圈
原题链接
P3199 [HNOI2009] 最小圈
思路分析
二分最小值x,那么原式变形为 Σ w - x <= 0
我们在原图以 w - x 为新边权找负环即可
我用的是bellman-ford,不过本题不卡spfa,如果换spfa可以获得更短的运行时间
时间复杂度:O(nm logW)
AC代码
#include <bits/stdc++.h>
using i64 = long long;
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int n, m;
std::cin >> n >> m;
std::vector<std::vector<std::pair<int, double>>> adj(n);
for (int i =0; i < m; ++ i) {
int u, v;
double w;
std::cin >> u >> v >> w;
-- u, -- v;
adj[u].emplace_back(v, w);
}
std::vector<double> dis(n);
auto Bellman_Ford = [&](double x) -> bool {
dis.assign(n, std::numeric_limits<double>::max());
dis[0] = 0;
bool flag = false;
for (int i = 0; i < n; i++)
{
flag = false;
for (int u = 0; u < n; ++ u)
{
if (dis[u] == std::numeric_limits<double>::max())
continue;
for (auto &[v, w] : adj[u])
{
if (dis[v] > dis[u] + w - x) {
dis[v] = dis[u] + w - x;
flag = true;
}
}
}
if (!flag)
return false;
}
return flag;
};
double lo = -1E7, hi = 1E7;
constexpr double eps = 1E-10;
while (lo + eps <= hi) {
double x = (lo + hi) / 2;
if (Bellman_Ford(x)) {
hi = x;
} else{
lo = x;
}
}
std::cout << std::fixed << std::setprecision(8) << hi << '\n';
return 0;
}
1.3.5 JSOI2016] 最佳团体
原题链接
P4322 [JSOI2016] 最佳团体
思路分析
树形背包 + 01分数规划
考虑从推荐人向被推荐人连有向边
二分最大值 M
那么有 Σ p - s * M >= 0
p - s * M 就是新的点权
我们在树上跑树形背包即可。 f(u, v) 为 以u 为根的子树,招募 v 个人的最大收益,需要一次dfs来预处理子树大小
为了减小常数,我们可以在第一次dfs的时候求dfs序列,然后在 dfs序列上跑树形背包。
AC代码
#include <bits/stdc++.h>
using i64 = long long;
constexpr double eps = 1E-6;
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
// sum(p) / sum(s)
int k, n;
std::cin >> k >> n;
++ k;
std::vector<int> s(n + 1), p(n + 1), r(n + 1), siz(n + 1);
std::vector<std::vector<int>> adj(n + 1);
for (int i = 1; i <= n; ++ i) {
std::cin >> s[i] >> p[i] >> r[i];
adj[r[i]].push_back(i);
}
std::vector<int> seq(n + 1);
int cur = 0;
auto dfs = [&](auto &&self, int u) -> void{
seq[cur ++] = u;
siz[u] = 1;
for (int v : adj[u]) {
self(self, v);
siz[u] += siz[v];
}
};
dfs(dfs, 0);
auto check = [&](double m) -> bool{
std::vector<std::vector<double>> f(n + 2, std::vector<double>(k + 1, std::numeric_limits<double>::lowest() / 2));
f[n + 1][0] = 0;
for (int i = n; i >= 0; -- i) {
f[i][0] = 0;
for (int j = k; j >= 1; -- j) {
f[i][j] = std::max(f[i + 1][j - 1] + (p[seq[i]] - s[seq[i]] * m), f[i + siz[seq[i]]][j]);
}
}
return f[0][k] >= 0;
};
double lo = 0, hi = std::accumulate(s.begin(), s.end(), 0.0);
while (lo + eps <= hi) {
double x = (lo + hi) / 2;
if (check(x)) {
lo = x;
} else {
hi = x;
}
}
std::cout << std::fixed << std::setprecision(3) << hi << '\n';
return 0;
}