【学习笔记】(16) 0/1 分数规划

发布时间 2023-05-31 17:06:22作者: jiangchenyangsong

1.算法介绍

分数规划用来求一个分式的极值。

比如说有 \(n\) 个物品,每个物品有两个权值 \(a\)\(b\) ,然后让你选出任意件数(但可能会有限制)的物品,使得两个权值和间的比值最大,即求 \(\dfrac{\sum_{i=1}^{k} a_i}{\sum_{i=1}^{k} b_i}\) (在这里 \(1−k\) 为挑出的 \(k\) 件物品)的最大值,然后对选择物品方面给出一定的限制条件,那么一道 0/1 分数规划的题目就完成了。

2.求解

2.1 二分法

假设我们要求上式的最大值。

再加一个限制 : \(k \ge W\)

那么令 \(sum = \sum_{i=1}^{k} a_i\), \(tot = \sum_{i=1}^{k} b_i\), 最优解为 \(ans\)

那么必然有:

\[\dfrac{sum}{tot} \le ans \]

化简得:

\[sum - ans \times tot \le 0 \]

带入得:

\[\sum_{i=1}^{k} (a_i - ans \times b_i) \le 0 \]

这样就把整体的贡献转化为了单件物品的贡献,则只需要二分这个 $ans $ 值,计算出每个物体的 \(a_i - ans \times b_i\), 然后从大到小排个序,取前 \(W\) 个看是否大于等于 0。(如果不能理解,也可以结合下面的图)

2.2 Dinkelbach 算法

二分法能通过大部分的题目,但效率有时太低了。

对上面的式子进行处理:

\[0 = \sum_{i=1}^{n} a_i \times x_i - ans \times \sum_{i=1}^{k} b_i \times x_i \]

其中 \(x_i\) 值为 \(0, 1\) 表示取或不取。

构造一个函数:

\[F(r) = \sum_{i=1}^{n}a_i \times x_i - r\times \sum_{i=1}^{n} b_i \times x_i \]

其中 \(F(r)\) 在平面坐标系上体现为一条直线,每一组 \(x_i\) 都分别唯一地对应一条直线,这些直线的截距均大于等于0、斜率均小于等于0。而这些直线在X轴上的截距就是这一组x求出来的r,而截距的最大值就是我们要求的R。(如下图所示)

图片引用自 https://www.cnblogs.com/KirisameMarisa/p/4187637.html

我们需要找到最大的 \(r\) 使得 \(F(r) = 0\) 即可。显然只需要判断最大的 \(F(r)\) 的正负性即可,最大的 \(F(r)\) 的求法上面也讲过了,取前 \(W\) 个小的。

我们在判断一个当前的 \(r\) 的时候需要去求一个 \(F(r)max\),在二分之中我们仅仅判断了 \(F(r)max\)\(0\) 的关系,这是利用率比较低的。其实我们可以将 \(F(r)max\) 利用起来。找到 \(F(r)max\) 所在的那一条直线,然后将 \(r\) 移动到这条直线的截距上面去(如下图,找到当前的 \(F(r)max\) 所在的直线,将 \(r\) 移动到 \(r4\) 上面去,这样做甚至只要 \(2\) 步即可到位)

3.例题

Ⅰ. 分数规划裸题 POJ2976 Dropping tests

板题。

点击查看代码
#include<cstdio>
#include<algorithm>
#define db double
#define eps 1e-6
using namespace std;
const int N = 1e3 + 67;
int read(){
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
	while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
	return x * f;
}
int n, k;
db a[N], b[N], c[N]; 
bool check(db x){
	for(int i = 1; i <= n; ++i) c[i] = a[i] - b[i] * x;
	sort(c + 1, c + 1 + n);
	db sum = 0;
	for(int i = k + 1; i <= n; ++i) sum += c[i];
	return sum >= 0;
}
signed main(){
	while(1){
		n = read(), k = read();
		if(!n && !k) exit(0);
		for(int i = 1; i <= n; ++i) a[i] = read();
		for(int i = 1; i <= n; ++i) b[i] = read();
		db l = 0, r = 1e6, ans = 0;
		while(r - l > eps){
			db mid = (l + r) / 2.0;
			if(check(mid)) ans = mid, l = mid;
			else r = mid;
		}
		printf("%.f\n", 100 * ans);
	}
	return 0;
}

Ⅱ.最优比率背包 P4377 [USACO18OPEN] Talent Show G

题目大意与上题类似,加了一个限制,要求 \(tot \ge W\)

那二分还是和之前一样,但是判断方式要发生改变,之前是贪心的取前 \(k\) 个,那现在有个 体积 限制,显然可以用背包来求解, 体积就是 \(b_i\), 价值就是 \(a_i - ans \times b_i\) ,跑 01 背包,判断正负性即可。

点击查看代码
#include<cstdio>
#include<algorithm>
#include<cstring>
#define inf -0x3f3f3f3f
#define int long long
using namespace std;
const int N = 1e4 + 67;
int read(){
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
	while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
	return x * f;
}
int n, V;
int w[N], t[N], f[N];
bool check(int x){
	memset(f, -0x3f, sizeof(f)); f[0] = 0;
	for(int i = 1; i <= n; ++i)
		for(int j = V; ~j; --j)
			if(f[j] != inf) f[min(V, j + w[i])] = max(f[min(V, j + w[i])], f[j] + t[i] - x * w[i]);
	return f[V] >= 0;
}
signed main(){
//	freopen("1.in", "r", stdin);
	n = read(), V = read();
	for(int i = 1; i <= n; ++i) w[i] = read(), t[i] = read() * 1000;
	int l = 0, r = 1e6, ans = 0;
	while(l <= r){
		int mid = (l + r) >> 1;
		if(check(mid)) ans = mid, l = mid + 1;
		else r = mid - 1;
	}
	printf("%lld\n", ans);
	return 0;
}

Ⅲ.最优比率生成树 POJ 2728 Desert King

对于构造最优比率生成树的分数规划,其实差不多类似是将 \(n\) 物品转换为 \(m\) 条边,然后求解,只不过限制改了,选出的 \(n−1\) 条边要构成一棵树

那么求法呢,其实也很类似:

就是令边权为 \(a_i - b_i \times ans\), 然后使用最小生成树算法,最后根据要求的是最大还是最小来判断正负。

(这道题好 sb 啊,为什么 %.3lf 和 $.3f 结果就不一样了呢?)

点击查看代码
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long
#define db double
#define eps 1e-5
using namespace std;
const int N = 2e3 + 67;
int read(){
	int x = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9'){if(ch == '-') f = -f; ch = getchar();}
	while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48); ch = getchar();}
	return x * f;
}
int n;
bool vis[N];
db f[N][N], g[N][N], minn[N];
db sum, tmp;
struct village{
	db x, y, z;
}a[N];
db Dis(village a, village b){
	return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
bool prim(db r){
	memset(vis, 0, sizeof(vis));
	sum = 0, vis[1] = 1;
	for(int i = 1; i <= n; ++i) minn[i] = g[1][i] - f[1][i] * r;
	for(int i = 2, k, j; i <= n; ++i){
		for(tmp = 1e18, k = i - 1, j = 2; j <= n; ++j)
			if(!vis[j] && minn[j] < tmp) tmp = minn[j], k = j;
		for(vis[k] = 1, sum += tmp, j = 2; j <= n; ++j)
			if(!vis[j]) minn[j] = min(minn[j], g[k][j] - f[k][j] * r);
	} 
	return sum - eps <= 0;
}
signed main(){
	while(1){
		n = read();
		if(!n) break;
		for(int i = 1; i <= n; ++i) a[i].x = read(), a[i].y = read(), a[i].z = read();
		for(int i = 1; i <= n; ++i)
			for(int j = i + 1; j <= n; ++j){
				f[i][j] = f[j][i] = Dis(a[i], a[j]);
				g[i][j] = g[j][i] = fabs(a[i].z - a[j].z);
			}
		db l = 0, r = 1e3, ans = 0;
		while(r - l > eps){
			db mid = (l + r) / 2.0;
			if(prim(mid)) ans = mid, r = mid;
			else l = mid;
		}
		printf("%.3f\n", ans);
	}
	return 0;
}

Ⅳ.最优比率环 P3199 [HNOI2009]最小圈

简而言之,最优比率环就是给你一个图,图上每条边有两个权值(当然,第二个权值可能恒为 1 ),然后让你在图中找到一个环,令环上边的个两个权值和的比值最大(或是最小)

然后就是要用上面生成树类似的思路,得到每条边边权为: \(a_i−b_i×ans\),然后在图上找负环(有时是正环,但是正负环可以人工转化的嘛),找到就 check 成功

远古马蜂

点击查看代码
#include<bits/stdc++.h>
#define N 3005
#define M 10005 
#define db double
using namespace std;
int read(){
	int x=0,f=1;char ch=getchar();
	while(ch<'0'||ch>'9'){if(ch=='-')f=-f;ch=getchar();}
	while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
	return x*f;
}
int n,m,tot,flag;
int Head[N],to[M],Next[M],vis[N];
db edge[M],d[N],ans;
void add(int u,int v,db w){to[++tot]=v,Next[tot]=Head[u],Head[u]=tot,edge[tot]=w;}
void dfs(int x,db xx){
	vis[x]=1;
	for(int i=Head[x];i;i=Next[i]){
		int y=to[i];
		if(d[y]>d[x]+edge[i]-xx){
			if(vis[y]||flag){flag=1;break;}
			d[y]=d[x]+edge[i]-xx;
			dfs(y,xx);
		}
	}
	vis[x]=0;
}
int main(){
	n=read(),m=read();
	for(int i=1;i<=m;++i){
		int u=read(),v=read();db w;scanf("%lf",&w),add(u,v,w);
	}
	db l=-1e7,r=1e7;
	while(r-l>1e-10){
		db mid=(l+r)/2;flag=0;
		memset(vis,0,sizeof(vis));
		for(int i=1;i<=n;i++) d[i]=0;
		for(int i=1;i<=n;i++){
			dfs(i,mid);if(flag) break;
		}
		if(flag) ans=mid,r=mid;
		else l=mid;
	}
	printf("%.8lf\n",ans);
	return 0;
}

Ⅴ.最大密度子图 UVA1389 Hard Life

参考:
https://www.cnblogs.com/KirisameMarisa/p/4187637.html
https://www.cnblogs.com/Judge/p/10173795.html