「笔记」01 分数规划

写在前面

0/1分数规划不需要去刻意记, 写这篇文章就是告诉大家这东西不用学。
——xwmwr

引入

AcWing 234. 放弃测试

以下题意高度抽象,建议先阅读原题面后再阅读简述题面。

多组数据,每次给定 \(n\) 个含有两个属性的元素 \((a_i, b_i)\),参数 \(k\)。求一组 \(w_i = \{0,1\}\),满足 \(\sum w_i \ge n-k\),最大化:

\[100\cdot \dfrac{\sum\limits_{i = 1}^{n}a_i\cdot w_i}{\sum\limits_{i=1}^{n} b_i\cdot w_i} \]

\(1\le k<n\le 10^3\)\(0\le a_\le b_i\le 10^9\)
1S,64MB。

这是一个 01 分数规划问题的典型:
每一个元素 \((a_i, b_i)\) 都有选(\(w_i=1\))或不选(\(w_i=0\))两种可能,要求最大化一个分数形式的值。

上述问题可以通过二分答案在 \(O(Txn\log n)\),其中 \(T\) 为数据组数,\(x\) 为二分次数。

求解

\(\frac{\sum a_i\cdot w_i}{\sum b_i\cdot w_i}\) 是存在单调性的,可以考虑二分答案。具体地,设当前枚举到 \(mid\),要求检查 \(\frac{\sum a_i\cdot w_i}{\sum b_i\cdot w_i}\ge mid\) 的合法性。
化下式子:

\[\begin{aligned} 100\cdot \dfrac{\sum\limits_{i = 1}^{n}a_i\cdot w_i}{\sum\limits_{i=1}^{n} b_i\cdot w_i} &\ge mid\\ 100\cdot {\sum\limits_{i = 1}^{n}a_i\cdot w_i} &\ge mid\sum\limits_{i=1}^{n} b_i\cdot w_i\\ {\sum\limits_{i = 1}^{n}w_i\cdot(100\cdot a_i - mid \cdot b_i)} &\ge 0 \end{aligned}\]

问题变为从数列 \(c_i = 100\cdot a_i - mid \cdot b_i\) 选出不少于 \(n-k\) 个元素,使它们的和不小于 0,排序后贪心即可。
总复杂度 \(O(Txn\log n)\),其中 \(T\) 为数据组数,\(x\) 为二分次数。

代码

//知识点:01 分数规划
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <iostream>
#define LL long long
const int kN = 1e3 + 10;
const double eps = 1e-8;
//=============================================================
int n, k, a[kN], b[kN];
double c[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) {
    w = (w << 3) + (w << 1) + (ch ^ '0');
  }
  return f * w;
}
bool Check(double mid_) {
  double sum = 0;
  for (int i = 1; i <= n; ++ i) c[i] = 100.0 * a[i] - mid_ * b[i];
  std::sort(c + 1, c + n + 1);
  for (int i = k + 1; i <= n; ++ i) sum += c[i];
  return sum >= eps;
}
int main() {
  while (true) {
    n = read(), k = read();
    if (n == 0 && k == 0) break;
    double ans = 0;
    for (int i = 1; i <= n; ++ i) a[i] = read();
    for (int i = 1; i <= n; ++ i) b[i] = read();
    for (double l = 0, r = 100; r - l >= eps; ) {
      double mid = (l + r) / 2.0;
      if (Check(mid)) {
        ans = l = mid;
      } else {
        r = mid;
      }
    }
    printf("%d\n", (int) (ans + 0.5));
  }
  system("pause");
  return 0;
}

小技巧

01 分数规划的答案可能在整数域上,不要死背模板。

例题

P7287 「EZEC-5」魔法

二分答案,01 分数规划

给定一长度为 \(n\) 的数列 \(A\),给定参数 \(s\),给定两种操作:

  1. 花费 \(a\) 的代价,将数列 \(A\) 中任意一个子串中的元素全部加 1。
  2. 花费 \(b\) 的代价,将数列 \(A\) 中任意一个子串中的元素全部乘 2。

两种操作进行的顺序任意,可以进行任意多次,求至少花费多少代价能使得数列 \(A\) 中存在一个子区间的元素之和不小于 \(s\)
\(1\le n\le 10^5\)\(1\le |A_i|,s,a,b\le 10^9\)
1S,128MB。

首先有两个显然的结论,所有操作 1 一定是全局使用的。操作 1 一定在 操作 2 之前。正确性显然,可以通过反证法得到不满足结论一定不会更优。
记操作 \(1\)\(2\) 进行的次数分别为 \(\operatorname{cnt}_a, \operatorname{cnt}_b\)。由于\(s\le 10^9\),则 \(\operatorname{cnt}_b\) 一定不大于 \(\log 10^9\) 次。
考虑枚举操作 \(b\) 进行的次数,问题转化为找到一个最小的非负整数 \(\operatorname{cnt}_a\),满足:

\[\exist 1\le l\le r\le n,\ 2^{\operatorname{cnt}_b}\times\sum_{i=l}^{r} (A_i + \operatorname{cnt}_a)\ge s \]

\(\operatorname{cnt}_a\) 越大,\(\sum_{i=l}^{r} (A_i + \operatorname{cnt}_a)\) 越大,上式左侧满足单调性。考虑二分 \(\operatorname{cnt}_a\),找到最小的满足上式的值即为最优的 \(\operatorname{cnt}_a\)Check\(O(n)\) 地求得新数列的最大子段和,检查是否满足上式即可。
总复杂度 \(O(n\log^2 w)\) 级别。


感谢涛哥!

这是使用 01 分数规划的理解方式。
考虑把上面的式子再化一下:

\[\begin{aligned} \exist 1\le l\le r\le n,\ 2^{\operatorname{cnt}_b}\times\sum_{i=l}^{r} (A_i + \operatorname{cnt}_a)&\ge s\\ (r - l + 1)\operatorname{cnt}_a + \sum_{i=l}^{r} A_i &\ge \dfrac{s}{2^{\operatorname{cnt}_b}}\\ \operatorname{cnt}_a&\ge \dfrac{\dfrac{s}{2^{\operatorname{cnt}_b}} - \sum\limits_{i=l}^{r} A_i}{r - l + 1} \end{aligned}\]

显然 \(\operatorname{cnt}_a = \frac{\frac{s}{2^{\operatorname{cnt}_b}} - \sum_{i=l}^{r} A_i}{r - l + 1}\) 时最优。发现这个式子是一个并不显然的 01 分数规划的形式。它可以看做有 \(n\) 个物品 \((A_i,1)\),钦定必须选连续的一段物品,最小化:

\[\dfrac{\dfrac{s}{2^{\operatorname{cnt}_b}} - \sum\limits_{i=1}^{n} w_i \cdot A_i}{\sum\limits_{i=1}^{n}w_i\cdot 1} \]

考虑二分答案最小化 \(\operatorname{cnt}_a\)

\[\begin{aligned} \dfrac{\dfrac{s}{2^{\operatorname{cnt}_b}} - \sum\limits_{i=l}^{r} A_i}{r - l + 1} &\le mid\\ \dfrac{s}{2^{\operatorname{cnt}_b}} - \sum\limits_{i=l}^{r} A_i &\le (r - l + 1)mid\\ \sum\limits_{i=l}^{r} (A_i + mid)&\ge \dfrac{s}{2^{\operatorname{cnt}_b}} \end{aligned}\]

得到了与上面本质相同的式子。

注意某些地方可能会炸 LL。

//知识点:二分答案,DP
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kMaxn = 1e5 + 10;
const LL kInf = 1e18 + 2077;
//=============================================================
LL n, a, b, s, ans = kInf, val[kMaxn];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(LL &fir_, LL sec_) {
  if (sec_ > fir_) fir_ = sec_;
}
void Chkmin(LL &fir_, LL sec_) {
  if (sec_ < fir_) fir_ = sec_;
}
bool Check(LL mid_, LL x_) {
  LL sum = 0;
  for (int i = 1; i <= n; ++ i) {
    if (sum > 0) {
      sum += val[i] + mid_;
    } else {
      sum = val[i] + mid_;
    }
    if (1.0 * sum >= 1.0 * s / x_) return true; //写成 (x_ * sum > s)炸 LL
  }
  return false;
}
//=============================================================
int main() {
  n = read(), a = read(), b = read(), s = read();
  for (int i = 1; i <= n; ++ i) val[i] = read();
  for (LL i = 0, x = 1; i <= 32; ++ i, x <<= 1ll) {
    LL numa = kInf;
    for (LL l = 0, r = kInf; l <= r; ) {
      LL mid = (l + r) >> 1ll;
      if (Check(mid, x)) {
        numa = mid;
        r = mid - 1;
      } else {
        l = mid + 1;
      }
    }
    Chkmin(ans, numa * a + b * i);
  }
  printf("%lld\n", ans);
  return 0;
}

「BJOI2019」奥术神杖

01 分数规划,AC 自动机。

没写 build 函数调一天哈哈

给定一只由数字和\(\texttt{.}\)构成的字符串 \(s\)。给定 \(m\) 个特殊串 \(t_{1}\sim t_{m}\)\(t_i\) 的权值为 \(v_i\)
需要在 \(s\) 中为\(\texttt{.}\)的位置上填入数字,一种填入方案的价值定义为:

\[\sqrt[c]{\prod_{i=1}^{c} w_i} \]

其中 \(w\) 表示在该填入方案中,出现过的特殊串的价值的可重集合,其大小为 \(c\)

每个位置填入的数字任意,最大化填入方案的价值,并输出任意一个方案。
\(1\le m,|s|,\sum|t_i|\le 1501\)\(1\le v_i\le 10^9\)
1S,512MB。

对于两种填入方案,我们只关心它们价值的相对大小。带着根号不易比较大小,套路地取个对数,之后化下式子:

\[\begin{aligned} \large \log {\sqrt[c]{\prod_{i=1}^{c} w_i}} =& \dfrac{\log {\left(\prod\limits_{i=1}^{c} w_i\right)}}{c}\\ =& \dfrac{\sum\limits_{i=1}^{c} \log {w_i}}{c} \end{aligned}\]

这是一个显然的 01 分数规划的形态,考虑二分答案。存在一种填入方案价值不小于 \(mid\) 的充要条件为:

\[\begin{aligned} \dfrac{\sum\limits_{i=1}^{c} \log {w_i}}{c}\ge mid \iff \sum\limits_{i=1}^{c}\left(\log {w_i} - mid\right)\ge 0 \end{aligned}\]


考虑 DP 检查二分量 \(mid\) 是否合法。
具体地,先将特殊串 \(t_i\) 的权值设为 \(\log v_i - mid\),更新 ACAM 上各状态的权值,之后在 ACAM 上模拟匹配过程套路 DP。
\(f_{i,j}\) 表示长度为 \(i\),在 ACAM 上匹配的结束状态为 \(j\) 的串的最大价值。
初始化 \(f_{0,0} = 0\),转移时枚举串长,状态,转移函数。注意某一位不为\(\texttt{.}\)时转移函数只能为串中的字符,则有:

\[f_{i,j} = \begin{cases} \max\limits_{\operatorname{trans}(u, s_i) = j} f_{i-1, u} + \operatorname{val}_{j} &(s_i\not= \texttt{.})\\ \max\limits_{\operatorname{trans}(u, k) = j} f_{i-1, u} + \operatorname{val}_{j} &(s_i= \texttt{.}) \end{cases}\]

注意记录转移时的前驱与转移函数,根据前驱还原出方案即可。
总复杂度 \(O(\left(10|s|\cdot\sum |t_i|\right)\log w)\) 级别,\(\log w\) 为二分次数。

//知识点:ACAM,分数规划
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <queue>
#define LL long long
#define DB double 
const int kN = 3e3 + 10;
const DB kInf = 1e10;
const DB eps = 1e-6;
//=============================================================
int n, m;
char origin[kN], s[kN], ans[kN];
//=============================================================
inline int read() {
  int f = 1, w = 0;
  char ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') f = -1;
  for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
  return f * w;
}
void Chkmax(int &fir, int sec) {
  if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
  if (sec < fir) fir = sec;
}
namespace ACAM {
  int node_num = 0, tr[kN][10], fail[kN], cnt[kN], from[kN][kN];
  DB sum[kN], val[kN], f[kN][kN];
  char ch[kN][kN];
  void Insert(char *s_, int val_) {
    int u_ = 0, lth = strlen(s_ + 1);
    for (int i = 1; i <= lth; ++ i) {
      if (! tr[u_][s_[i] - '0']) tr[u_][s_[i] - '0'] = ++ node_num;
      u_ = tr[u_][s_[i] - '0'];
    }
    sum[u_] += log(val_);
    cnt[u_] ++;
  }
  void Build() {
    std::queue <int> q;
    for (int i = 0; i < 10; ++ i) {
      if (tr[0][i]) q.push(tr[0][i]);
    }
    while (! q.empty()) {
      int u_ = q.front(); q.pop();
      for (int i = 0; i < 10; ++ i) {
        int v_ = tr[u_][i];
        if (v_) {
          fail[v_] = tr[fail[u_]][i];
          sum[v_] += sum[fail[v_]];
          cnt[v_] += cnt[fail[v_]];
          q.push(v_);
        } else {
          tr[u_][i] = tr[fail[u_]][i];
        }
      }
    }
  }
  bool DP(DB mid_) {
    //初始化
    for (int i = 0; i <= node_num; ++ i) val[i] = sum[i] - cnt[i] * mid_;
    for (int i = 0; i <= n; ++ i) {
      for (int j = 0; j <= node_num; ++ j) {
        f[i][j] = -kInf;
      }
    }
    f[0][0] = 0;

    //DP
    for (int i = 0; i < n; ++ i) {
      for (int j = 0; j <= node_num; ++ j) {
        if (f[i][j] == -kInf) continue;
        if (origin[i + 1] == '.') {
          for (int k = 0; k < 10; ++ k) {
            int v_ = tr[j][k];
            if (f[i + 1][v_] < f[i][j] + val[v_]) {
              f[i + 1][v_] = f[i][j] + val[v_];
              from[i + 1][v_] = j;
              ch[i + 1][v_] = k + '0';
            }
          }
        } else {
          int v_ = tr[j][origin[i + 1] - '0'];
          if (f[i + 1][v_] < f[i][j] + val[v_]) {
            f[i + 1][v_] = f[i][j] + val[v_];
            from[i + 1][v_] = j;
            ch[i + 1][v_] = origin[i + 1];
          }
        }
      }
    }

    //寻找最优解
    int pos = 0;
    for (int i = 0; i <= node_num; ++ i) {
      if (f[n][i] > f[n][pos]) pos = i;
    }
    if (f[n][pos] <= 0) return false;
    for (int i = n, j = pos; i; -- i) {
      ans[i] = ch[i][j];
      j = from[i][j];
    }
    return true;
  }
}
//=============================================================
int main() {
  n = read(), m = read();
  scanf("%s", origin + 1);
  for (int i = 1; i <= m; ++ i) {
    scanf("%s", s + 1);
    int val = read();
    ACAM::Insert(s, val);
  }
  ACAM::Build();
  for (DB l = 0, r = log(kInf); r - l >= eps; ) {
    DB mid = (l + r) / 2.0;
    if (ACAM::DP(mid)) {
      l = mid;
    } else {
      r = mid;
    }
  }
  printf("%s", ans + 1);
  return 0; 
}
posted @ 2021-01-11 22:30  Luckyblock  阅读(121)  评论(0编辑  收藏  举报