01分数规划
upd.2026.8.2 昨天做 ABC 的题碰到了,再补一下之前没学彻底的知识点。
基础知识
定义
$01$ 分数规划问题主要包含一般的 $01$ 分数规划、最优比率生成树问题、最优比率环问题、最大密度子图等。我们将会对这四个问题进行讨论。
解决这类问题有两种方法:二分法,$Dinkelbach$ 算法,下文我只会用二分法(通常也用二分法)。
$Dinkelbach$ 算法在这里看:0/1 分数规划详解 - 洛谷专栏(消失的图在这看:0/1分数规划?我不会啊! - Jμdge - 博客园)
所谓的 $01$ 分数规划问题就是指这样的一类问题,给定两个数组,$a_i$ 表示选取 $i$ 的收益,$b_i$ 表示选取 $i$ 的代价。如果选取 $i$,定义 $x_i = 1$,否则 $x_i = 0$。
每一个物品只有选或者不选两种方案,求一个选择方案使得 $R = \frac{\sum a_i \times x_i}{\sum b_i \times x_i}$ 取得最值,即所有选择物品的总收益/总代价的值最大或是最小。一般来讲就是求一个最优比率。
当然选择物品的时候可能会有限制。
观察形式会发现,出现一类求分式最值的问题,不妨考虑 $01$ 分数规划。
推导
永远要记得,我们的目标是使 $R$ 取到最值($R$ 在不同问题中可能不一样,取的是最大最小值也不一定),一定要记住这一点,在下文我会反复提及。
那么如何让 $R$ 取到最值?我们可以通过数学推导来求出来。
我们先不管每个物品选和不选,假设当前找到了一组选择 $R = \frac{\sum a_i}{\sum b_i}$。
我们再假定现在有一个解 $\mathit{ans}$。
那必然可以有:$R \ge \mathit{ans}$。
由于我们想让 $R$ 最大,那么就让 $\mathit{ans}$ 尽可能大。所以我们考虑二分 $\mathit{ans}$ 即可。
当然,在此之前我们要整理一下这个式子方便后续的二分,注意由于涉及除法,一般二分都要用分数二分,注意精度问题。
$\frac{\sum a_i}{\sum b_i} \ge \mathit{ans}$。
$\sum a_i \ge \sum b_i \times \mathit{ans}$。
$\sum a_i - \sum b_i \times \mathit{ans} \ge 0$,倒数第二步。
$\sum \left( a_i - b_i \times \mathit{ans} \right) \ge 0$,最后一步。
到此,就可以了。
从倒数第二步推到最后一步。
$\sum b_i \times \mathit{ans} = \sum \left( b_i \times \mathit{ans} \right)$。
你可能不知道为什么这样是正确的,很简单,我们简单证明下。
$\sum b_i \times \mathit{ans} = \left( b_1 + b_2 + \dots + b_n \right) \times \mathit{ans} = \left( b_1 \times \mathit{ans} + b_2 \times \mathit{ans} + \dots + b_n \times \mathit{ans} \right)$。
$\sum \left( b_i \times \mathit{ans} \right) = \left( b_1 \times \mathit{ans} + b_2 \times \mathit{ans} + \dots + b_n \times \mathit{ans} \right)$。
好了,证毕。
还有一个注意点,这里一定要推到最后一步,因为这样才能把独立的变量 $a_i$,$b_i$,$ans$ 变为整体 $\left( a_i - b_i \times \mathit{ans} \right)$,从而更方便处理。
因为我们一开始假定了有一组选择再往下推的,现在需要思考如何选。
由于已经把问题转化为存在性了,也就是判断是否存在一组选择(也就是找到一组选择即可),使得上面的那个式子成立即可。
这里就用 check 完成即可。
这里可以换另外一种方法理解,假设一开始并不知道某组解,那可以把问题描述成:
求出一组 $x_i$ 使得 $R = \frac{\sum a_i \times x_i}{\sum b_i \times x_i}$ 取得最值。
依旧考虑变形,最终变成:$\sum \left( x_i [ a_i - b_i \times \mathit{ans} \right] ) \ge 0$。
那也就是考虑对于 $ans$,check 的本质就是能否找出一组 $x_i$ 满足上述式子即可。
部分代码
这里贴上分数二分的板子:
double R=1e8, L=0;
while (R-L>1e-5)
{
double mid=(L+R)/2;
if (check(mid))
{
ans=mid;
L=mid;
}
else R=mid;
}
至于 check 函数,就是我们上述要求的:
$\sum \left( a_i - b_i \times \mathit{ans} \right) \ge 0$
看看这个式子是否成立,也就是左式是否大于 $0$。
check 函数的写法有很多,因题而异,例如:贪心,dp,背包,等等等等
可能与什么知识点结合
分数规划一般来讲不会单独成题,一般来讲有以下几种形式:
0.不与任何算法结合,即分数规划裸题
1.与 01 背包结合,即最优比率背包
2.与生成树结合,即最优比率生成树
3.与负环判定结合,即最优比率环
4.与网络流结合,即最大密度子图
5.与其他的各种带选择的算法乱套,即最优比率啥啥的...
例如:
6.与费用流结合,即最优比率流(这个是我乱叫的)
下面是具体例题。
1.分数规划裸题(贪心)
题目要求 $n$ 个物品中选 $m$ 个,使得 $\frac{\sum v_i}{\sum c_i}$ 最大化,这样看上去,不就是上述推导吗?
不过多了一个限制条件,我们就不重新推式子了,还是这个式子。
$$\sum \left( v_i - c_i \times \mathit{ans} \right) \ge 0$$
想让这个式子最大,可以用贪心的思想,给每个这样的值排个序,取前 $m$ 个相加即可,很简单。
#include <bits/stdc++.h>
using namespace std;
const int N=205;
int n, m, v[N], c[N];
double a[N], ans=0;
bool cmp(double a, double b)
{
return a>b;
}
bool check(double x)
{
double res=0;
for (int i=1; i<=n; i++) a[i]=v[i]*1.0-c[i]*x*1.0;
sort(a+1, a+1+n, cmp);
for (int i=1; i<=m; i++) res+=a[i];
return res>=0.0;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i=1; i<=n; i++) scanf("%d", &v[i]);
for (int i=1; i<=n; i++) scanf("%d", &c[i]);
double R=1e5, L=0;
while (R-L>1e-5)
{
double mid=(L+R)/2;
if (check(mid))
{
ans=mid;
L=mid;
}
else R=mid;
}
printf("%.3lf", ans);
return 0;
}
2.最优比率生成树(最小生成树)
P4951 [USACO01OPEN] Earthquake - 洛谷
题目要使 $\frac{f - \sum c_i}{\sum t_i}$ 最大化,且这些值只在每条边上,要选出 $n-1$ 条边使图连通的前提下满足这个式子最大值。
乍一看式子改变了,限制条件也变了,那不简单?重推一遍就好了。
假定现问题有一个答案是 $\mathit{ans}$。
$\frac{f - \sum c_i}{\sum t_i} \ge \mathit{ans}$
$f - \sum c_i \ge \sum t_i \times \mathit{ans}$
$f - \sum c_i - \sum t_i \times \mathit{ans} \ge 0$
$f - \sum \left( c_i - t_i \times \mathit{ans} \right) \ge 0$
令 $\mathit{res} = \sum \left( c_i - t_i \times \mathit{ans} \right)$,即 $f - \mathit{res} \ge 0$。
由于在这个式子中,要使得成立,应该让 $\mathit{res}$ 尽可能的小,注意我们 $\mathit{check}$ 函数求的就是 $\mathit{res}$,所以这里求的时候是要求最小而不是上题中的最大了!
我们现在就考虑 $\mathit{check}$ 函数如何写了。这里因为要使图连通,且满足这个式子最小,那就可以考虑用最小生成树了。
注意 $\mathit{check}$ 函数,因为要调用多次,所以要提前保留边的信息($u$,$v$),不然每次进行排序都会打乱这些信息,所以每次进入 $\mathit{check}$ 要初始化这些边的信息!!!
#include <bits/stdc++.h>
using namespace std;
const int N=10005;
struct node
{
int u, v;
double w;
}a[N];
int n, m, k, u1[N], v1[N], c[N], t[N], cnt=0, f[N];
double sum=0, ans=0;
int find(int x)
{
if (f[x]==x) return x;
return f[x]=find(f[x]);
}
bool cmp(node a, node b)
{
return a.w<b.w;
}
bool check(double x)
{
for (int i=1; i<=m; i++)
{
a[i].w=c[i]*1.0+t[i]*1.0*x;
a[i].u=u1[i], a[i].v=v1[i];
}
sort(a+1, a+1+m, cmp);
for (int i=1; i<=n; i++) f[i]=i;
cnt=0, sum=0;
for (int i=1; i<=m; i++)
{
int u=a[i].u, v=a[i].v;
double w=a[i].w;
int fu=find(u), fv=find(v);
if (fu!=fv)
{
f[fu]=fv;
cnt++, sum+=w;
}
if (cnt==n-1) break;
}
return (k-sum)>=0;
}
int main()
{
scanf("%d%d%d", &n, &m, &k);
for (int i=1; i<=m; i++) scanf("%d%d%d%d", &u1[i], &v1[i], &c[i], &t[i]);
double R=1e12, L=0;
while (R-L>1e-8)
{
double mid=(R+L)/2;
if (check(mid))
{
ans=mid;
L=mid;
}
else R=mid;
}
printf("%.4lf", ans);
return 0;
}
AT_arc026_4 [ARC026D] 道を直すお仕事 - 洛谷
假设能够找到一种修复方案,即 $\frac{\sum c_i}{\sum t_i}$。
再假设存在解 $ans$,即有:$\frac{\sum c_i}{\sum t_i} \le ans$。
处理一下式子,得到:$\sum(c_i - ans\times t_i) \le 0$。
再考虑 check。
发现可以利用贪心思想,负边权一定选,对于正边权再依次考虑。
也就是相当于最小生成树处理即可。
3.最优比率背包(背包)
P4377 [USACO18OPEN] Talent Show G - 洛谷(01背包)
这题有一个限制条件,物品重量要 $\ge W$,那么看到物品重量,很容易和背包联想在一起。我们可以用背包来处理这个重量问题。
我们要求 $\frac{\sum t}{\sum w}$ 的最大值,那就把 $\mathit{check}$ 函数处理换成背包处理。
再推一遍式子。
假定现问题有一个答案是 $\mathit{ans}$。
$\frac{\sum t}{\sum w} \ge \mathit{ans}$
$\sum t \ge \sum w \times \mathit{ans}$
$\sum t - \sum w \times \mathit{ans} \ge 0$
$\sum \left( t - w \times \mathit{ans} \right) \ge 0$
那么我们现在搞个 $01$ 背包,来满足这个式子。
$f_w$ 表示重量为 $w$ 时,最大化 $\sum \left( t - w \times \mathit{ans} \right)$。
最后看看是否 $\ge 0$ 即可。
#include <bits/stdc++.h>
using namespace std;
const int N=255, M=250005;
int n, m, w[N], t[N];
double ans=0, f[M];
bool check(double x)
{
for (int i=1; i<=m; i++) f[i]=-1e9;
for (int i=1; i<=n; i++)
for (int j=m; j>=0; j--)
{
if (j+w[i]>=m) f[m]=max(f[m], f[j]+t[i]*1.0-w[i]*1.0*x);
else f[j+w[i]]=max(f[j+w[i]], f[j]+t[i]*1.0-w[i]*1.0*x);
}
return f[m]>=0.0;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i=1; i<=n; i++) scanf("%d%d", &w[i], &t[i]);
double R=1e7, L=0;
while (R-L>1e-5)
{
double mid=(L+R)/2;
if (check(mid))
{
ans=mid;
L=mid;
}
else R=mid;
}
printf("%d", (int)(ans*1000));
return 0;
}
F-红的愿望_牛客挑战赛34(树上背包)
4.最优比率环(负环)
$E$ 是一个环,题目要求 $\frac{\sum e_w}{|E|}$ 最小值,其中 $e \in E$,$e_w$ 表示环内边权,$|E|$ 表示环内点数。
假定现问题有一个答案是 $ans$。
$\frac{\sum e_w}{|E|} \ge ans$
$\sum e_w \ge ans \times |E|$
$\sum e_w - ans \times |E|\ge 0$
因为我们钦定找到了一组解,所以 $E$ 已经固定,从而每一个边 $e \in E$ 也固定,所以把 $ans$ 放到求和里面是正确的,即:
$\sum (e_w - ans)\ge 0$
再考虑 check,也就是判能不能找一个环,使得环的边权之和 $\ge 0$,也就是找个负环就行。
5.最优比率流(网络流)
然而我不会。鸽子。
可以转为类似方格取数的题。(有 n*m 的方格,每个格子有权值 a[i][j],拿走格子 (i,j) 上的权后,第 i 行和第 j 列的格子都不能再取,求一共取 k 次能获得的最大权值和。)
然后不会了。
6.杂题
P1419 寻找段落 - 洛谷(与滑动窗口结合)
假设能够找到一组解,即找到了 $[L, R]$,且满足 $S \le R-L+1 \le T$。
再假设存在解 $ans$,满足 $\frac{sum_R-sum_{L-1}}{R-(L-1)} \ge ans$。
$sum_R-sum_{L-1} \ge ans \times [R-(L-1)]$
$sum_R-sum_{L-1} - ans \times [R-(L-1)]\ge 0$
再考虑把式子凑成一个方便考虑的整体,即:$sum_R-ans \times R - [sum_{L-1}-ans\times(L-1)]\ge 0$
那式子变形就做完了,思考如何才能找出这样一种 $[L, R]$。
令 $t_i=sum_i-ans\times i$,式子变成是否能找出 $t_R \ge t_L$。
对于每一个 $R$,其 $L$ 都有一段范围限定,只需找出所有 $t_L$ 的最小值即可,这里就可以用滑动窗口维护。
E - Pro Exam Eligibility(与尺取法结合)
和上一题类似处理,前面半段处理方法相同。具体如下:
假设能够找到一组解,即找到了 $[L, R]$,且满足 $S \le R-L+1 \le T$。
再假设存在解 $ans$,满足 $\frac{sum_R-sum_{L-1}}{R-(L-1)} \ge ans$。
最后把式子变形处理得:$sum_R-ans \times R - [sum_{L-1}-ans\times(L-1)]\ge 0$。
思考如何才能找出这样一种 $[L, R]$。
同样的,令 $t_i=sum_i-ans\times i$,式子变成是否能找出 $t_R \ge t_L$。
对于每一个 $R$,其 $L$ 都有一段范围限定,不过注意到这个范围从 $1$ 开始,故而尺取法维护即可。
AT_abc324_f [ABC324F] Beautiful Path - 洛谷(求最短路)
假设能够找到一条从 $1$ 到 $N$ 的路径,即 $\frac{\sum b_i}{\sum c_i}$。
再假设存在解 $ans$,即有:$\frac{\sum b_i}{\sum c_i} \ge ans$。
处理一下式子,得到:$\sum(b_i - ans\times c_i) \ge 0$。
check 也就是找最长路即可。
但是这题限制了 $u_i < v_i$,是个 DAG,所以拓扑+DP即可。

浙公网安备 33010602011771号