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

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

update on 2023.6.2 等我复习完网络流再回来

Ⅵ.最优比率流 P3705 [SDOI2017] 新生舞会

类似 方格取数 和 0/1 分数规划的结合体。

所以 二分 套上一个 网络流 即可。

这里使用费用流, 每行向 源点 连边,边权 1, 费用 0, 每列向 汇点 连边,边权 1, 费用 \(a_i - b_i \times ans\)。

double 类型居然不能 memset, 长见识了。

点击查看代码
#include<bits/stdc++.h>
#define db double
#define eps 1e-7
#define INF 0x3f3f3f3f
using namespace std;
const int N = 1e3 + 67, M = 2e5 + 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, s, t;
bool vis[N];
db res = 0, val[M << 1], a[N][N], b[N][N], dis[N];
int tot, Head[N], to[M << 1], Next[M << 1], edge[M << 1], now[N];
void add(int u, int v, int w, db vv){
	to[++tot] = v, Next[tot] = Head[u], Head[u] = tot, edge[tot] = w, val[tot] = vv;
	to[++tot] = u, Next[tot] = Head[v], Head[v] = tot, edge[tot] = 0, val[tot] = -vv;
}
bool spfa(){
	for(int i = 1; i <= t; ++i) vis[i] = 0, dis[i] = -INF;
	queue<int> q;
	dis[s] = 0, q.push(s), now[s] = Head[s], vis[s] = 1;
	while(!q.empty()){
		int x = q.front(); q.pop(), vis[x] = 0;
		for(int i = Head[x]; i; i = Next[i]){
			int y = to[i]; if(!edge[i]) continue; 
			if(dis[y] < dis[x] + val[i]){
				now[y] = Head[y];
				dis[y] = dis[x] + val[i];
				if(!vis[y]) q.push(y), vis[y] = 1;
			}
		}
	}
	return dis[t] != -INF;
}
int dinic(int x, int flow){
	vis[x] = 1; if(x == t) return flow;
	int rest = flow;
	for(int i = now[x]; i && rest; i = Next[i]){
		int y = to[i]; now[x] = i;
		if((vis[y] && y != t) || !edge[i] || dis[y] != dis[x] + val[i]) continue;
		int k = dinic(y, min(rest, edge[i]));
		if(k) res += val[i] * k, edge[i] -= k, edge[i ^ 1] += k, rest -= k;
	}
	return flow - rest;
}
bool check(db r){
	res = 0;
	memset(Head, 0, sizeof(Head)), tot = 1;
	for(int i = 1; i <= n; ++i) add(s, i, 1, 0), add(i + n, t, 1, 0);
	for(int i = 1; i <= n; ++i)
		for(int j = 1; j <= n; ++j)
			add(i, j + n, 1, a[i][j] - b[i][j] * r);
	int flow = 0;
	while(spfa()){
		vis[t] = 1;
		while(vis[t]){
			memset(vis, 0, sizeof(vis));
			flow += dinic(s, INF);
		}
	}
	return res >= 0;
}
int main(){
	n = read(); s = n * 2 + 1, t = n * 2 + 2; 
	for(int i = 1; i <= n; ++i) for(int j = 1; j <= n; ++j) a[i][j] = read();
	for(int i = 1; i <= n; ++i) for(int j = 1; j <= n; ++j) b[i][j] = read();
	db l = 0, r = 1e4, ans = 0;
	while(r - l > eps){
		db mid = (l + r) / 2.0;
		if(check(mid)) l = mid, ans = mid;
		else r = mid;
	}
	printf("%.6lf\n", ans);
	return 0;
}

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

posted @ 2023-05-31 16:56  Aurora-JC  阅读(148)  评论(0)    收藏  举报