Loading

分数规划 基本题型&模板

分数规划 基本题型&模板

定义

01分数规划是这样的一类问题,有一堆物品,每一个物品有一个收益\(ai\),一个代价\(bi\),我们要求一个方案,选出k个物品,使选择的\(\sum{ai}/\sum{bi}\)最大。

基本做法是采用二分法,假设当前二分到的答案为x,那么:

\(\sum{ai}/\sum{bi}>=x\)可以转化为:\(\sum{(ai-x*b_i)}>=0\),所以每次check的时候将\(a_i-x*b_i\)进行排序,取前k个,判断他们的和是否大于等于0即可

模板

poj2976

给出两个数组a和b,\(a_i\)代表这门课获得的成绩,\(b_i\)代表这门课的满分是多少,现在要求你舍弃掉k门课,使得平均绩点(\(100*\sum{ai}/\sum{bi}\))最高。

直接二分即可,注意是舍弃k门课,所以相当于选择n-k门课,另外输出需要四舍五入,可以直接利用.0lf进行四舍五入

#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <algorithm>
#include <iostream>
#include <limits>
#include <map>
#include <set>
#include <vector>

using namespace std;

const int N = 1e6 + 5;
typedef long long LL;
int k, n;
double a[N], b[N], c[N];

const double eps = 1e-8;
// 和0做比较
int sgn(double x) {
    if (fabs(x) < eps) return 0;  // =0
    if (x < 0)
        return -1;  // < 0
    else
        return 1;  // > 0
}

bool check(double mid) {
    for (int i = 0; i < n; i++) c[i] = a[i] - mid * b[i];
    sort(c, c + n);
    double sum = 0;
    for (int i = n - 1; i >= k; i--) sum += c[i];  //选n-k项
    return (sgn(sum) >= 0);
}

int main() {
    while (scanf("%d%d",&n,&k)) {
        if ((n + k) == 0) break;
        for (int i = 0; i < n; i++) scanf("%lf", &a[i]);
        for (int i = 0; i < n; i++) scanf("%lf", &b[i]);
        double l = 0, r = 1;           //成绩最大是1,也就是满分
        while (r - l > 1e-8) {         // r与l的间隔小于1e-8
            double mid = (l + r) / 2;  // 不需要考虑加一的事情
            if (check(mid))
                l = mid;
            else
                r = mid;  // r和l都是mid}
        }
        printf("%.0lf\n",l*100);//四舍五入
    }
    return 0;
}

例题

最优比率生成环

acwing361观光奶牛
题意: 给定一张L个点、P条边的有向图,每个点都有一个权值f[i],每条边都有一个权值t[i]。求图中的一个环,使“环上各点的权值之和”除以“环上各边的权值之和”最大。输出这个最大值。 点数N~1e3, 边数M~5e3
题解: 二分枚举答案,然后根据这个mid来重新构图,与\(a_i-mid*b_i\)类似,本题为$ f[t]-midw[i]\(的和是否大于0,那么将每个点的权值下放到边上,然后判断是否存在正环即可,也可以变个符号,判断是否存在负环:具体操作就是spfa时当对t点的所有出边进行更新的时候,原来的边权w[i],变为\)midw[i] - f[t]$

#include <bits/stdc++.h>

using namespace std;

int const N = 1e3 + 10, M = 5e5 + 10;
double const eps = 1e-8;
int e[M], ne[M], w[M], idx, h[N], n, m, cnt[N], st[N], f[N];
double dist[N];

// 建邻接表
void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

// spfa求负环(正环)
bool spfa(double mid) {
    queue<int> q;
    
    memset(dist, 0x3f, sizeof dist);
    memset(cnt, 0, sizeof cnt);
    memset(st, 0, sizeof st);
    for (int i = 1; i <= n; i ++ ) {
        st[i] = true;
        q.push(i);
    }
    // dist[0] = 0, st[0] = 1, q.push(0);  如果希望能够正确求出dis
    while (q.size())  {
        int t = q.front();  // 取队首
        q.pop();  // 出队首

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i]) {
            int j = e[i];
            if (dist[j] > dist[t] + w[i] * mid - f[t]) { // 这里是判断负环,如果是判正环:1.初始化写成memset(dis, 0xc0, sizeof dis), 2.更新条件写成dist[j] < dist[t] + w[i]
                dist[j] = dist[t] + w[i] * mid - f[t];  // 边权发生改变
                cnt[j] = cnt[t] + 1;  // 更新边数
                if (cnt[j] >= n) return true;  // 如果j点到源点的边数大于等于n
                if (!st[j]) {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return false;
}

int main() {
    cin >> n >> m;
    memset(h, -1, sizeof h);
    for (int i = 1; i <= n; ++i) scanf("%d", &f[i]);
    for (int i = 1, a, b, c; i <= m; ++i) {
        scanf("%d %d %d", &a, &b, &c);
        add(a, b, c);
    }
    
    double l = 0, r = 1e9;
    while (r - l > eps) {
        double mid = (l + r) / 2;
        if (spfa(mid)) l = mid;
        else r = mid;
    }
    printf("%.2lf", l);
    return 0;
}

最优比率生成树

acwing348 沙漠之王
题意: 大卫希望渠道的总成本和总长度的比值能够达到最小。他只希望建立必要的渠道,为所有的村庄提供水资源,这意味着每个村庄都有且仅有一条路径连接至首都。他的工程师对所有村庄的地理位置和高度都做了调查,发现所有渠道必须直接在两个村庄之间水平建造。由于任意两个村庄的高度均不同,所以每个渠道都需要安装一个垂直的升降机,从而使得水能够上升或下降。建设渠道的成本只跟升降机的高度有关,换句话说只和渠道连接的两个村庄的高度差有关。需注意,所有村庄(包括首都)的高度都不同,不同渠道之间不能共享升降机。
题解:
要求找出一棵\((a1/b1) + (a2/b2) +... + (an/bn)\)之和最大生成树
我们设\((a1/b1) + (a2/b2) +... + (an/bn) = mid\)。那么对应于每一条边我们可以得到一条新边\(ai-mid * bi\),采用二分的方式枚举mid,如果得到的使用新边\(ai-mid * bi\)建成的最小生成树的权值之和为0,那么这个mid就是我们的答案;否则,找其他的mid

#include <bits/stdc++.h>

using namespace std;

int const N = 2e3 + 10;

int n;
double dis[N], d[N][N], h[N][N]; // dis记录到最小生成树的最小距离,d数组记录两个点的最小距离,h数组记录两个点的最小高度
double x[N], y[N], z[N]; // 记录每个点输入的位置
bool vis[N]; // 判断每个点是否在最小生成树内

// prime算法查找新边建立的最小生成树是否满足条件
bool prime (double mid) {
    memset(dis, 0x3f, sizeof dis);
    memset(vis, 0, sizeof vis);
    double sum = 0; // 最小生成树的边权值和
    vis[1] = 1;
    
    // 把1号点放入集合后,计算和1号点相邻的所有点的距离
    for (int i = 2; i <= n; ++i)
        dis[i] = h[1][i] - mid * d[1][i];
    
    for (int i = 2; i <= n; ++i) {
        // 找出到最小生成树距离最小的那个点
        double mini = 0x3f3f3f3f;
        int u = -1;;
        for (int j = 2; j <= n; ++j) {
            if (!vis[j] && dis[j] < mini) {
                mini = dis[j], u = j;
            }
        }
        
        // 放入最小生成树内
        vis[u] = 1;
        sum += dis[u];
        
        // 更新所有与u点相邻的点
        for (int j = 2; j <= n; ++j) {
            if (!vis[j] && dis[j] > h[u][j] - mid * d[u][j])
                dis[j] = h[u][j] - mid * d[u][j];  // 用广义边更新
        }
    }
    
    // 判断是否满足条件
    if (sum >= 0) return false;  // mid取太小
    else return true;
}

int main() {
    while (scanf("%d", &n) != EOF && n) {
        memset(x, 0, sizeof x);
        memset(y, 0, sizeof y);
        memset(z, 0, sizeof z);
        memset(d, 0, sizeof d);
        memset(h, 0, sizeof h);
        for (int i = 1; i <= n; ++i) {
            cin >> x[i] >> y[i] >> z[i];
            // 建立一张完全图
            for (int j = 1; j < i; ++j) {
                d[i][j] = d[j][i] = sqrt((x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j]));  // 记录i点和前j个点的距离
                h[i][j] = h[j][i] = fabs(z[i] - z[j]); // 记录高度差
            }
        }
        
        // 01分数规划,二分查找答案
        double l = 0, r = 1000.0, mid;
        while (r - l > 1e-6) {
             mid = (l + r) / 2;
             if (prime(mid)) r = mid; // 完全图采用prime算法
             else l = mid;
        }
        printf("%.3f\n", mid);
    }
    return 0;
}

树形背包01分数规划

P1642 规划

某地方有N个工厂,有N-1条路连接它们,且它们两两都可达。每个工厂都有一个产量值和一个污染值。现在工厂要进行规划,拆除其中的M个工厂,使得剩下的工厂依然连成一片且 总产量/总污染 的值最大。

还是分数规划的基本套路,二分mid,然后去跑树形背包即可

#include <bits/stdc++.h>

using namespace std;

const int N = 1e2 + 5;
typedef long long LL;
int n, m, a[N], b[N];
vector<int> mp[N];
double dp[N][N];

const double eps = 1e-8;
// 和0做比较
int sgn(double x) {
    if (fabs(x) < eps) return 0;  // =0
    if (x < 0)
        return -1;  // < 0
    else
        return 1;  // > 0
}
int sz[N];
void dfs(int now, int fa, double mid) {
    dp[now][0] = 0;
    sz[now] = 1;
    for (int i = 0; i < mp[now].size(); i++) {
        int son = mp[now][i];
        if (son == fa) continue;
        dfs(son, now, mid);
        sz[now] += sz[son];
        for (int j = min(m, sz[now]) - 1; j >= 0; j--)
            for (int k = 0; k <= min(j, sz[son]); ++k)
                dp[now][j] = max(dp[now][j], dp[now][j - k] + dp[son][k]);
    }
    for (int i = min(m, sz[now]); i >= 1; --i)
        dp[now][i] = dp[now][i - 1] + 1.0 * a[now] - mid * b[now];
}

bool check(double mid) {
    for (int i = 0; i <= n;i++){
        for (int j = 0; j <= m; j++) dp[i][j] = -1e18;
    }
    dfs(1, 0, mid);
    for (int i = 1; i <= n; i++)
        if (sgn(dp[i][m]) >= 0) return true;
    return false;
}

int main() {
    cin >> n >> m;
    m = n - m;  //拆除m个,也就是选择n-m个
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= n; i++) cin >> b[i];
    for (int i = 0; i < n - 1; i++) {
        int x, y;
        cin >> x >> y;
        mp[x].push_back(y), mp[y].push_back(x);
    }
    double l = 0, r = 100000;
    while (r - l > 1e-8) {         // r与l的间隔小于1e-8
        double mid = (l + r) / 2;  // 不需要考虑加一的事情
        if (check(mid))
            l = mid;
        else
            r = mid;  
    }
    printf("%.1lf\n", l);
    return 0;
}
posted @ 2021-03-15 11:45  dyhaohaoxuexi  阅读(114)  评论(0编辑  收藏  举报