01分数规划

01分数规划属于二分法的一个应用,主要用于解决有关 “最优比率” 的问题,如最优比率背包、最优比率生成树等。

题目大致是说,给定两个长度均为 n 的数组 ab,要从中选出 kab,求 maxi=1naisii=1nbisi。其中 si=0(不选) 或 si=1(选),且 i=1nsi=k

考虑如何求解。

最暴力的方法就是二进制枚举,复杂度 O(2n),时间太劣。

我们考虑去猜数,猜测所求的值 c,使得:

i=1naisii=1nbisic

移项可得:

i=1naisic×i=1nbisi

i=1naisii=1n(c×bisi)

i=1n(aisic×bisi)0

i=1nsi(aic×bi)0

此时我们要对每一个 i 求出 aic×bi,记为 di。那么显然,取 di 最大的 k 个元素最优。

此外,当 c 单调递增时,di 是单调递减的。于是我们可以通过二分提高猜数的效率,大幅缩短程序运行时间。

总时间复杂度 O(nlogn)

P4377 [USACO18OPEN] Talent Show G

这道题将01分数规划与01背包结合,求 maxi=1ntisii=1nwisi,此外还有 i=1nwisiW 的限制。

因为 i=1nwisi 可以大于 W,所以在统计背包的时候,要将大于 W 的背包容量统一成 W 记录下来。

最后如果 fW<0,说明答案估大了,否则估小了。

代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

const int kmax = 255;
const int kmaxM = 1005;

struct Cow {
  int w, t;
  double v;
} c[kmax];

int n, w;
double l, r;
double f[kmaxM];

bool Check(double x) {
  for (int i = 1; i <= n; i++) {
    c[i].v = c[i].t - x * c[i].w; // 求值
  }
  f[0] = 0;
  for (int i = 1; i <= w; i++) {
    f[i] = -1e18;
  }
  for (int i = 1; i <= n; i++) { // 背包
    for (int j = w; ~j; j--) {
      f[min(w, j + c[i].w)] = max(f[min(w, j + c[i].w)], f[j] + c[i].v);
    }
  }
  return f[w] < 0;
}

int main() {
  cin >> n >> w;
  for (int i = 1; i <= n; i++) {
    cin >> c[i].w >> c[i].t;
    r += c[i].t; // 计算上限
  }
  for (int i = 1; i <= 60; i++) { //限制二分次数
    double mid = (l + r) / 2.0;
    if (Check(mid)) {
      r = mid;
    } else {
      l = mid;
    }
  }
  cout << (long long)(1000ll * l);
  return 0;
}

P4322 [JSOI2016] 最佳团体

这道题要在01分数规划下做树上背包。

fx,y 表示在 x 的子树中一共选 y 个人最大的结果。

注意 0 号点要算进去,共选 m+1 人,dp 的终态是 f0,m+1

代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <vector>

using namespace std;

const int kmax = 2505;
const double eps = 1e-4; // 精度

struct Person {
  int c, p;
  double v;
} p[kmax];

int m, n;
int siz[kmax];
double l, r;
double f[kmax][kmax];
vector<int> e[kmax];

void Dfs(int x, int fa) {
  f[x][1] = p[x].v; // 只选他自己一个人
  siz[x] = 1;
  for (int y : e[x]) {
    if (y == fa) continue;
    Dfs(y, x);
    for (int i = min(m + 1, siz[x] + siz[y]); i; i--) {
      for (int j = 0; j <= min(i - 1, siz[y]); j++) {
        f[x][i] = max(f[x][i], f[x][i - j] + f[y][j]);
      }
    }
    siz[x] += siz[y];
  }
}

bool Check(double x) {
  for (int i = 1; i <= n; i++) {
    p[i].v = p[i].p - x * p[i].c;
  }
  for (int i = 0; i <= n; i++) {
    for (int j = 1; j <= m + 1; j++) {
      f[i][j] = -1e18;
    }
  }
  Dfs(0, 0);
  return f[0][m + 1] <= 0;
}

int main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> m >> n;
  for (int i = 1, x; i <= n; i++) {
    cin >> p[i].c >> p[i].p >> x;
    e[x].push_back(i);
    r += p[i].p;
  }
  for (double mid; l + eps < r;) {
    mid = (l + r) / 2.0;
    if (Check(mid)) {
      r = mid;
    } else {
      l = mid;
    }
  }
  cout << fixed << setprecision(3) << l;
  return 0;
}

P3199 [HNOI2009] 最小圈

简化题意:给定一张有向图,定义一个环的价值为环上边的边权平均值,求这张图所有环的价值最小值。

假设一个环有 k 条边,其权值为 i=1kwkk,我们猜测一个数 x,使得:

i=1kwkkx

i=1kwkkx

(i=1kwk)kx0

i=1k(wkx)0

也就是说,将原来每条边的边权减去 x 后,如果有向图中存在负环,不等式是合法的。

这里同样可以采用二分提高效率。

判负环时以每个点为起点,如果对同一点更新了多次答案,说明存在负环。

代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <vector>

using namespace std;

const int kmax = 3005;
const double eps = 1e-9;

struct E {
  int y;
  double w;
};

int n, m;
double l = -1e6, r = 1e6;
double d[kmax];
bool b[kmax], o;
vector<E> e[kmax];

void Spfa(int x, double k) {
  b[x] = 1;
  for (auto cur : e[x]) {
    int y = cur.y;
    double w = cur.w - k;
    if (d[y] > d[x] + w) {
      d[y] = d[x] + w;
      if (b[y]) {
        o = 1;
        return;
      }
      Spfa(y, k);
    }
  }
  b[x] = 0;
}

bool Check(double k) {
  o = 0;
  memset(b, 0, sizeof(b));
  memset(d, 0, sizeof(d));
  for (int i = 1; i <= n && !o; i++) {
    Spfa(i, k);
  }
  return o;
}

int main() {
  cin >> n >> m;
  for (int i = 1, x, y; i <= m; i++) {
    double w;
    cin >> x >> y >> w;
    e[x].push_back({y, w});
  }
  for (double mid; l + eps < r;) {
    mid = (l + r) / 2.0;
    if (Check(mid)) {
      r = mid;
    } else {
      l = mid;
    }
  }
  cout << fixed << setprecision(8) << r << '\n';
  return 0;
}

P3288 [SCOI2014] 方伯伯运椰子

题目中的两种调整方式分别对应退流增广。最优方案是在最大流不变的情况下使压缩、扩容总费用和最少。

更形式化点说,存在残量网络使得:

  1. 扩容: uv 花费 b+d
  2. 压缩:(反向边有流量)满足 c>0vu 花费 ab

然后套用分数规划,同上题思路判负环即可。

为什么可行,需要借助一个定理 —— 消圈定理。

在某个流 f 中,若它对应的残余网络没有负环,那么它一定就是当前流量下的最小费用流。

代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <vector>

using namespace std;

const int kmax = 5005;
const double eps = 1e-4;

struct E {
  int y;
  double w;
};

int n, m;
vector<E> e[kmax];
bool b[kmax], o;
double d[kmax];

void Spfa(int x, double k) {
  b[x] = 1;
  for (auto cur : e[x]) {
    int y = cur.y;
    double w = cur.w + k;
    if (d[y] > d[x] + w) {
      d[y] = d[x] + w;
      if (b[y]) { // 再次被更新
        o = 1; // 说明存在负环
        return;
      }
      Spfa(y, k);
    }
  }
  b[x] = 0;
}

bool Check(double k) {
  memset(b, 0, sizeof(b));
  memset(d, 0, sizeof(d));
  o = 0;
  for (int i = 1; i <= n && !o; i++) {
    Spfa(i, k); // 判负环
  }
  return o;
}

double Search() { // 二分
  double l = 0, r = 1e9;
  for (double mid; l + eps < r;) {
    mid = (l + r) / 2.0;
    if (Check(mid)) {
      l = mid;
    } else {
      r = mid;
    }
  }
  return l;
}

int main() {
  cin >> n >> m;
  n += 2;
  for (int i = 1, x, y, a, b, c, d; i <= m; i++) {
    cin >> x >> y >> a >> b >> c >> d;
    e[x].push_back({y, (double)(b + d)}); // 建边
    if (c > 0) e[y].push_back({x, (double(a - d))}); 
  }
  cout << fixed << setprecision(2) << Search();
  return 0;
}

完结撒花 \ / \ / \ /

posted @   ereoth  阅读(188)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示