基础算法——分数规划 学习笔记
基础算法——分数规划 学习笔记
引入
分数规划用来求一个分式的极值。
具体的,给定 \(n\) 个元素,每个元素有属性 \(a_i,b_i\),求一个集合 \(P\in[1,n]\),最大/最小化比率:$$\dfrac{\sum_{i\in P}a_i}{\sum_{i\in P}b_i}$$
求解
二分法
假设我们要求最大值(求最小值的方法和求最大值的方法类似),二分一个 \(\text{mid}\),然后推式子(省略范围):
设 \(g(x)=\sum(a_i-b_i\times x)\),则原问题其实就是最大化 \(g(x)\)。可以用二分求解:二分一个 \(\text{mid}\),如果 \(g(\text{mid})\ge0\),则说明可行;否则就是不可行。
实际求解的时候一般是,如果 \(g(\text{mid})>0\),那么就 \(l\leftarrow\text{mid}+1\),否则 \(r\leftarrow\text{mid}\)。
在题目中,一般来说,分数规划的难点在于求解 \(g(x)\) 的最大/最小值。
模型
01 分数规划
题意:在 \(n\) 个物品中选取 \(k\) 个,使得比率最大。
分析:二分最大的比率为 \(\text{mid}\),最大化 \(g(\text{mid})=\sum(a_i-b_i\times\text{mid})\);可以贪心来考虑,将 \(a_i-b_i\times\text{mid}\) 视为物品的权值,将权值前 \(k\) 大的数加起来,即 \(g(\text{mid})\);如果 \(g(\text{mid})>0\),则 \(l\leftarrow\text{mid}+1\),否则 \(r\leftarrow\text{mid}\)。
变式:选取若干个,最大化比率。贪心考虑,只取所有权值为正的物品,其余同上。
代码:
using db = double; const db eps = 1e-6; int n, k, a[1010], b[1010]; db c[1010]; inline bool cmp(const db a, const db b) { return a > b; } bool check(const db x) { for (int i = 1; i <= n; ++i) c[i] = a[i] - b[i] * x; sort(c + 1, c + n + 1, cmp); db s = 0; for (int i = 1; i <= n - k; ++i) s += c[i]; return s > 0; } signed main() { while (1) { n = rr, k = rr; if (!n && !k) break; for (int i = 1; i <= n; ++i) a[i] = rr; for (int i = 1; i <= n; ++i) b[i] = rr; db l = 0, r = 100, mid; while (r - l > eps) { mid = (l + r) / 2; if (check(mid)) l = mid; else r = mid; } printf("%d\n", int(mid * 100 + 0.5)); } return 0; }
最优比率生成树
题意:给定一棵图,每条边有两个权值 \(a_i,b_i\),求一棵生成树,使得 \(\dfrac{\sum_{e\in T}a_e}{\sum_{e\in T}b_e}\) 最小。
分析:与上一题很类似,把 \(a_i-b_i\times\text{mid}\) 作为每条边的权值,那么这个图的最小生成树就是 \(f(\text{mid})\) 最小值
代码:略。
最优比率生成环
题意:给定一个图,每条边有价值 \(v_i\) 和费用 \(p_i\),求一个环,使得总价值与总费用的比值最小。
分析:我们二分一个答案 \(\text{mid}\),如果存在一个环,满足 \(\sum v/\sum p\ge\text{mid}\),那么移项一下就有 \(\sum v\ge\text{mid}\times\sum p\),就有 \(\sum v\ge\sum(\text{mid}\times p)\),也即存在一个环满足 \(\sum(\text{mid}\times p-v)\le0\),也就是把 \(\text{mid}\times p-v\) 作为新的边权,要判断图中是否存在负环。找负环可以用 SPFA 的 DFS 版本。
代码:
int SPFA(int u, double mid) { // 判负环 vis[u] = 1; for (int i = head[u]; i; i = e[i].nxt) { int v = e[i].v; double w = e[i].w - mid; if (dis[u] + w < dis[v]) { dis[v] = dis[u] + w; if (vis[v] || SPFA(v, mid)) return 1; } } vis[u] = 0; return 0; } bool check(double mid) { // 如果有负环返回 true for (int i = 1; i <= n; ++i) dis[i] = 0, vis[i] = 0; for (int i = 1; i <= n; ++i) if (SPFA(i, mid)) return 1; return 0; }
应用
例题
题目:AT_abc324_f Beautiful Path(考前打比赛遇到的)。
题意:给定一个 \(n\) 个点 \(m\) 条边的有向图,保证 \(\forall i\in[1,n]:u_i<v_i\)。每条边有价值 \(b_i\) 和费用 \(c_i\),找一条路径 \(1\rightarrow n\),最大化 \(\sum b_i/\sum c_i\)。
分析:二分一个答案 \(\text{ans}\),则有 \(\sum b_i/\sum c_i\ge\text{ans}\longrightarrow\sum b_i\ge\text{ans}\times\sum c_i\longrightarrow\sum b_i-\text{ans}\times\sum c_i\ge0\longrightarrow\sum(b_i-\text{ans}\times c_i)\ge0\),因此,以 \(b_i-\text{ans}\times c_i\) 为边权建图跑最短路即可。
代码:
const int N = 2e5 + 10; const db eps = 1e-10, db INF = 1e18; int n, m; struct node { int v, b, c; }; vector<node> g[N]; db f[N]; bool check(db x) { for (int i = 1; i <= n; ++i) f[i] = -INF; f[1] = 0; for (int i = 1; i <= n; ++i) for (node j : g[i]) f[j.v] = max(f[j.v], f[i] + j.b - j.c * x); return f[n] > 0; } signed main() { n = rr, m = rr; for (int i = 1, u, v, b, c; i <= m; ++i) { u = rr, v = rr, b = rr, c = rr; g[u].push_back({v, b, c}); } db l = 0, r = 1e4; while (r - l > eps) { db mid = (l + r) / 2; if (check(mid)) l = mid; else r = mid; } printf("%.15lf", l); return 0; }
练习题
见:https://www.luogu.com.cn/training/400462
Reference
[1] https://oi-wiki.org/misc/frac-programming/
[2] https://www.cnblogs.com/captain1/p/9929128.html
本文来自博客园,作者:RainPPR,转载请注明原文链接:https://www.cnblogs.com/RainPPR/p/frac-programming.html
如有侵权请联系我(或 2125773894@qq.com)删除。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战