*【学习笔记】(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\):
那么必然有:
化简得:
带入得:
这样就把整体的贡献转化为了单件物品的贡献,则只需要二分这个 $ans $ 值,计算出每个物体的 \(a_i - ans \times b_i\), 然后从大到小排个序,取前 \(W\) 个看是否大于等于 0。(如果不能理解,也可以结合下面的图)
2.2 Dinkelbach 算法
二分法能通过大部分的题目,但效率有时太低了。
对上面的式子进行处理:
其中 \(x_i\) 值为 \(0, 1\) 表示取或不取。
构造一个函数:
其中 \(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