分数规划

\(\text{poj-2728}\)

给定 \(n\) 个点的坐标和点权,任意两点之间的边的价值是它们的距离,费用是两点权值之差的绝对值,求该图的一棵生成树,使得该树所有边的费用之和与价值之和的比值最小(只需求这个比值即可)。

\(2 \le n \le 1000\)\(0 \le x_i, y_i \le 10^4\)\(0 \le w_i \le 10^9\)


事实上就是求下式的最小值,\(w_i \in \{0, 1\}\)

\[\frac{\sum\limits_{i=1}^n a_i \times w_i}{\sum\limits_{i=1}^n b_i \times w_i} \]

这类问题称为分数规划,通用方法就是二分答案,设当前二分的答案为 \(mid\),则:

\[\begin{align} \frac{\sum a_i \times w_i}{\sum b_i \times w_i} &\ge mid \\ \sum a_i \times w_i - mid \times \sum b_i \times w_i &\ge 0 \\ \sum w_i \times (a_i - mid \times b_i) &\ge 0 \end{align} \]

即不等式成立则说明 \(mid\) 合法,因此,这道题要求最小值,也就是求 \(mid\) 合法最大值。

于是二分答案,然后用 \(\text{Prim}\) 算法 \(O(n^2)\) \(\text{check}\) 即可。

总体时间复杂度为 \(O(n^2 \log (\sum w_i))\)

注意:这道题 \(\text{Kruskal}\)\(\text{TLE}\);不要用 \(\text{memset()}\) 初始化 \(\text{double}\) 变量 \(\dots\)

#include<iostream>
#include<cstdio>
#include<cmath>
#include<vector>
#include<cstring>
using namespace std;
#define MAXN 5005

int read() {
	int x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

int n, x[MAXN], y[MAXN], p[MAXN];
double dis[MAXN];
bool vis[MAXN];

bool Prim(double mid) {
	for(int i = 1; i <= n; i ++) dis[i] = 1e18, vis[i] = false;
	dis[1] = 0; double res = 0;
	for(int i = 1; i < n; i ++) {
		int mk = 0;
		for(int j = 1; j <= n; j ++) if(!vis[j] && 
			(dis[j] < dis[mk] || !mk)) mk = j;
		vis[mk] = true;
		for(int j = 1; j <= n; j ++) if(!vis[j])
			dis[j] = min(dis[j], abs(p[j] - p[mk]) - mid * sqrt((x[mk] - x[j]) * (x[mk] - x[j]) + (y[mk] - y[j]) * (y[mk] - y[j])));
	}
	for(int i = 2; i <= n; i ++) res += dis[i];
	return (res >= 0);
}

int main() {
	while(cin >> n) {
		if(!n) break;
		for(int i = 1; i <= n; i ++) cin >> x[i] >> y[i] >> p[i];
		double l = 0, r = 1e9, eps = 1e-8;
		while(r - l >= eps) {
			double mid = (l + r) / 2.0;
			if(Prim(mid)) l = mid;
			else r = mid;
		}
		printf("%.3lf\n", l);
	}
	return 0;
}

\(\text{loj-149}\)

给你 \(n\) 个物品,每个物品有两个属性 \(a_i\)\(b_i\),求一组解 \(x_i(1\le i\le n, x_i=0\)\(1)\) 使

\[\frac{\sum\limits a_i\times x_i}{\sum\limits b_i\times x_i} \]

最大,且恰好有 \(k\)\(x_i\)\(1\)。请求出这个最大值。如果你的答案与标准答案的绝对误差在 \(5\times 10^{-5}\) 以内,你的答案则被视为是正确答案。


01 分数规划模板题。

考虑二分答案,求的就是最大的 \(mid\),满足存在一种 \(x_i\) 使得下式成立:

\[\begin{align} \frac{\sum a_i\times x_i}{\sum b_i\times x_i} &\ge mid \\ \sum a_i \cdot x_i &\ge mid \times \sum b_i \cdot x_i \\ \sum x_i \times (a_i - mid \cdot b_i) &\ge 0 \end{align} \]

二分 check 直接贪心即可,按 \(a_i - mid \cdot b_i\) 排序,选前 \(k\) 个。

#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define ll long long
#define MAXN 100005

ll read() {
	ll x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

ll n, k, a[MAXN], b[MAXN];
double c[MAXN];

bool check(double x) {
	double res = 0;
	for(int i = 1; i <= n; i ++) c[i] = a[i] - x * b[i];
	sort(c + 1, c + n + 1);
	for(int i = n; i >= n - k + 1; i --) res += c[i];
	return (res >= 0);
}

int main() {
	n = read(), k = read();
	for(int i = 1; i <= n; i ++) a[i] = read();
	for(int i = 1; i <= n; i ++) b[i] = read();
	double l = 0, r = 1;
	while(r - l > 1e-10) {
		double mid = (l + r) / 2.0;
		if(check(mid)) l = mid;
		else r = mid;
	}
	printf("%.8lf\n", l);
	return 0;
}

\(\text{luogu-3199}\)

考虑带权有向图 \(G=(V,E)\) 以及 \(w:E\rightarrow \R\),每条边 \(e=(i,j)\)\(i\neq j\)\(i, j\in V\))的权值定义为 \(w_{i,j}\)。设 \(n=|V|\)

\(c=(c_1,c_2,\cdots,c_k)\)\(c_i\in V\))是 \(G\) 中的一个圈当且仅当 \((c_i,c_{i+1})\)\(1\le i<k\))和 \((c_k,c_1)\) 都在 \(E\) 中。称 \(k\) 为圈 \(c\) 的长度,同时记 \(c_{k+1}=c_1\),并定义圈 \(c=(c_1,c_2,\cdots,c_k)\) 的平均值为

\[\mu(c)= \frac 1 k \sum\limits_{i=1}^{k} w_{c_i,c_{i+1}} \]

\(c\) 上所有边的权值的平均值。设 \(\mu'(G)=\min_c\mu(c)\)\(G\) 中所有圈 \(c\) 的平均值的最小值。

给定图 \(G=(V,E)\) 以及 \(w:E\rightarrow \R\),求出 \(G\) 中所有圈 \(c\) 的平均值的最小值 \(\mu'(G)\)

\(2\leq n\le 3000\)\(1\leq m\le 10000\)\(|w_{i,j}| \le 10^7\)\(1\leq i, j\leq n\)\(i\neq j\)


这道题有两种解法,第一种直接用结论,第二种就是常规做法。

以下部分题解来自于 题解 P3199 HNOI2009 最小圈 - 洛谷专栏

对于这个东西,有一个结论(Karp1977年的论文):

我们新建一个节点,从它到每个点连一条权值任意的边(比如都是 \(0\)),再令 \(F_j(i)\) 表示从新建的点到 \(i\) 点恰好经过 \(j\) 条边的最短路,那么有:

\[ans=\min_{1\leq i\leq n, F_{n+1}(i)\neq\infty}\max_{j=1}^{n}\left[\frac{F_{n+1}(v)-F_k(v)}{n+1-k}\right] \]

具体证明请看博客。虽然代码不是跑的最快的但是理论复杂度低,是 \(O(nm)\) 的。

#include<iostream>
#include<cstdio>
using namespace std;
#define ll long long
#define MAXN 3005
#define MAXM 10005
#define INF 0x3f3f3f3f

ll read() {
	ll x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

double f[MAXN][MAXN], w[MAXM];
ll n, m, a[MAXM], b[MAXM];

int main() {
	n = read(), m = read();
	for(int i = 0; i < m; i ++) 
		a[i] = read(), b[i] = read(), cin >> w[i];
	for(int i = 1; i <= n; i ++) 
		for(int j = 1; j <= n; j ++) f[i][j] = INF;
	for(int i = 0; i < n; i ++) for(int j = 0; j < m; j ++)
		f[i + 1][b[j]] = min(f[i + 1][b[j]], f[i][a[j]] + w[j]);
	double res = INF, t = 0;
	for(int i = 1; i <= n; i ++) if(f[n][i] < INF) {
		t = -INF;
		for(int j = 0; j < n; j ++) 
			t = max(t, (f[n][i] - f[j][i]) / (n - j));
		res = min(res, t);
	}
	printf("%.8lf\n", res);
	return 0;
}

以下部分题解来自于 题解 P3199 HNOI2009 最小圈 - 洛谷专栏

负环 + 分数规划。这道题的做法和 P3288 [SCOI2014]方伯伯运椰子 比较相似,所以做着比较快。我们依然要求最小的 \(C=\frac{\sum\limits_{i=1}^{k}a'[i]}{\sum\limits_{i=1}^{k}b'[i]}, b[i]=1\),也就是最优比率环,可以得到一个分数规划模型,化一下可以得到 \(\sum\limits_{i=1}^{k}a'[i]-C\sum\limits_{i=1}^{k}b'[i]=0\),因为 \(b'[i]=1\),得到 \(\sum\limits_{i=1}^{k}a'[i]-C=0\),所以我们将所有边权 \(-C\),最后判一下是否有负环即可。

没写这个解法的代码,就不放了。

\(\text{luogu-4377}\)

Farmer John 要带着他的 \(n\) 头奶牛,方便起见编号为 \(1\ldots n\),到农业展览会上去,参加每年的达牛秀!他的第 \(i\) 头奶牛重量为 \(w_i\),才艺水平为 \(t_i\),两者都是整数。

在到达时,Farmer John 就被今年达牛秀的新规则吓到了:

(一)参加比赛的一组奶牛必须总重量至少为 \(W\)(这是为了确保是强大的队伍在比赛,而不仅是强大的某头奶牛),并且。

(二)总才艺值与总重量的比值最大的一组获得胜利。

FJ 注意到他的所有奶牛的总重量不小于 \(W\),所以他能够派出符合规则(一)的队伍。帮助他确定这样的队伍中能够达到的最佳的才艺与重量的比值。

\(1 \leq n \leq 250\)\(1 \leq W \leq 1000\)\(1 \leq w_i \leq 10^6\)\(1 \leq t_i \leq 10^3\)


可以转化为分数规划问题,本题对于 check 有额外限制,跑 01 背包即可。

#include<iostream>
#include<cstdio>
using namespace std;
#define ll long long
#define MAXN 1005
#define INF 0x3f3f3f3f

ll read() {
	ll x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

ll n, W, w[MAXN], t[MAXN];
double dp[MAXN], v[MAXN];

bool check(double x) {
	for(int i = 1; i <= n; i ++) v[i] = t[i] - x * w[i];
	for(int i = 1; i <= W; i ++) dp[i] = -INF;
	for(int i = 1; i <= n; i ++) for(int j = W; j >= 0; j --)
		if(j + w[i] >= W) dp[W] = max(dp[W], dp[j] + v[i]);
		else dp[j + w[i]] = max(dp[j + w[i]], dp[j] + v[i]);
	return (dp[W] >= 0);
}

int main() {
	n = read(), W = read();
	for(int i = 1; i <= n; i ++) w[i] = read(), t[i] = read();
	double l = 0, r = 1e9;
	while(r - l > 1e-8) {
		double mid = (l + r) / 2.0;
		if(check(mid)) l = mid;
		else r = mid;
	}
	l += 1e-8; cout << (ll)(l * 1000) << "\n";
	return 0;
}
posted @ 2026-03-21 15:34  So_noSlack  阅读(4)  评论(0)    收藏  举报