01 分数规划

1 问题概述

分数规划是用于求一类分式的极值问题。

给定两个数列 \(a_i,b_i\),求出一个数列 \(w_i\in \{0,1\}\),最小(大)化下列式子:

\[\dfrac{\sum\limits_{i=1}^na_i\times w_i}{\sum\limits_{i=1}^nb_i\times w_i} \]

再说直白点就是每个物品有 \(a,b\) 两个权值,选出当中一些物品,使 \(\dfrac{\sum a}{\sum b}\) 最小(大)。

2 解法

解决 01 分数规划问题的通法就是二分。下面以最大值举例说明。

我们二分一个答案 \(mid\),如果要求的分式值可以大于这个 \(mid\) 那么就说明可行,转移二分区间并记录答案。于是我们就可以推出下面式子:

\[\dfrac{\sum a_i\times w_i}{\sum b_i\times w_i}>mid\\ \sum a_i\times w_i-mid\times\sum b_i\times w_i>0\\ \sum w_i\times (a_i-mid\times b_i)>0 \]

所以我们只需要求出 \(\sum w_i\times (a_i-mid\times b_i)\) 的最大值,如果比 \(0\) 大就代表可行,否则一定不可行。

下面举一些例子来说明 01 分数规划的题目的特征。

3 实例

3.1 [USACO18OPEN] Talent Show G

首先转化题目模型如下:

求出 \(\dfrac{\sum t_i}{\sum w_i}\) 的最大值,要满足 \(\sum w_i\ge W\)

发现要求式子就是 01 分数规划的标准形式,现在问题是怎样求 \(\sum (t_i-mid\times w_i)\) 最大值,还需要保证 \(\sum w_i\ge W\)

仔细思考一下我们不难发现,如果我们构造一个物品,体积为 \(w_i\),价值为 \((t_i-mid\times w_i)\),这个问题就转化为了一个相当朴素的 dp —— 01 背包。所以我们直接套在二分中即可。

代码:

#include <bits/stdc++.h>
#define int long long

using namespace std;

const int Maxn = 2e5 + 5;
const int Inf = 2e9;
const int base = 10000;

int n, W;
int w[Maxn], t[Maxn];

int sum = 0, sw = 0;

int dp[255][2505];
bool check(int x) {
	memset(dp, 128, sizeof dp);
	dp[0][0] = 0;
	for(int i = 0; i < n; i++) {
		for(int j = 0; j <= W; j++) {
			int k = min(W, j + w[i + 1]);
			dp[i + 1][k] = max(dp[i + 1][k], dp[i][j] + t[i + 1] - x * w[i + 1]);
			dp[i + 1][j] = max(dp[i + 1][j], dp[i][j]);
		}
	}
	return dp[n][W] > 0;
}

int ans = 0;
void bs() {
	int l = 0, r = sum, mid;
	while(l <= r) {
		mid = (l + r) >> 1;
		if(check(mid)) {
			ans = mid;
			l = mid + 1;
		}
		else {
			r = mid - 1;
		}
	}
}

signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> W;
	for(int i = 1; i <= n; i++) {
		cin >> w[i] >> t[i];
		t[i] *= base; 
		sum += t[i];
		sw += w[i];
	}
	bs();
	cout << ans / 10;
	return 0;
}

3.2 [HNOI2009] 最小圈

首先转化模型如下:

每一条边上有两个权值 \(w_i,b_i\),且 \(b_i=1\)。求出一个环使得 \(\dfrac{\sum w_i}{\sum b_i}\) 最小。

显然这又是一个 01 分数规划问题,我们最后转化出的式子应该是 \(\sum(w_i-mid\times b_i)<0\),也就是 \(\sum(w_i-mid)<0\)

我们将边权赋为 \((w_i-mid)\),然后就是要求一个环使得边权和为负。直接利用 SPFA 判负环即可。

代码如下:

#include <bits/stdc++.h>

using namespace std;

const int Maxn = 2e5 + 5;
const int Inf = 2e9;

int n, m;
struct Edge {
	int u, v;
	double w;
}ed[Maxn];

int head[Maxn], edgenum;
struct node {
	int nxt, to;
	double w;
}edge[Maxn];

void add(int u, int v, double w) {
	edge[++edgenum] = {head[u], v, w};
	head[u] = edgenum;
}

double dis[Maxn];
bool vis[Maxn];
bool SPFA(int x) {
	vis[x] = 1;
	for(int i = head[x]; i; i = edge[i].nxt) {
		int to = edge[i].to;
		double w = edge[i].w;
		if(dis[x] + w < dis[to]) {
			dis[to] = dis[x] + w;
			if(vis[to] || SPFA(to)) {
				return 1;
			}
		}
	} 
	vis[x] = 0;
	return 0;
}

int check(double x) {
	edgenum = 0;
	for(int i = 1; i <= n; i++) {
		head[i] = 0;
	}
	for(int i = 1; i <= m; i++) {
		add(ed[i].u, ed[i].v, ed[i].w - x);
	}
	for(int i = 1; i <= n; i++) {
		dis[i] = 0;
		vis[i] = 0;
	}
	for(int i = 1; i <= n; i++) {
		if(SPFA(i)) {
			return 1;
		}
	}
	return 0;
}

double bs() {
	double l = -1e7, r = 1e7, mid;
	while(r - l >= 1e-9) {
		mid = (l + r) / 2.0;
		if(check(mid)) {
			r = mid;
		}
		else {
			l = mid;
		}
	}
	return l;
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	cin >> n >> m;
	for(int i = 1; i <= m; i++) {
		cin >> ed[i].u >> ed[i].v >> ed[i].w;
		add(ed[i].u, ed[i].v, ed[i].w);
	}
	double res = bs();
	cout << fixed << setprecision(8) << res ;
	return 0;
}

通过上述实例不难发现,当题目中出现两个数列和之比,也就是形如 \(\dfrac{\sum a_i}{\sum b_i}\) 的时候,我们往往考虑 01 分数规划。当我们通过二分将问题转化为 \(\sum(a_i-mid\times b_i)\) 的最值与 \(0\) 的关系之后,主要的问题就在于怎样求 \(\sum(a_i-mid\times b_i)\) 的最值了。

posted @ 2024-06-02 09:27  UKE_Automation  阅读(61)  评论(0)    收藏  举报