「笔记」01 分数规划
写在前面
0/1分数规划不需要去刻意记, 写这篇文章就是告诉大家这东西不用学。
——xwmwr
引入
以下题意高度抽象,建议先阅读原题面后再阅读简述题面。
多组数据,每次给定 \(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\) 的合法性。
化下式子:
问题变为从数列 \(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\),给定两种操作:
- 花费 \(a\) 的代价,将数列 \(A\) 中任意一个子串中的元素全部加 1。
- 花费 \(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\),满足:
\(\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 分数规划的理解方式。
考虑把上面的式子再化一下:
显然 \(\operatorname{cnt}_a = \frac{\frac{s}{2^{\operatorname{cnt}_b}} - \sum_{i=l}^{r} A_i}{r - l + 1}\) 时最优。发现这个式子是一个并不显然的 01 分数规划的形式。它可以看做有 \(n\) 个物品 \((A_i,1)\),钦定必须选连续的一段物品,最小化:
考虑二分答案最小化 \(\operatorname{cnt}_a\)。
得到了与上面本质相同的式子。
注意某些地方可能会炸 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。
对于两种填入方案,我们只关心它们价值的相对大小。带着根号不易比较大小,套路地取个对数,之后化下式子:
这是一个显然的 01 分数规划的形态,考虑二分答案。存在一种填入方案价值不小于 \(mid\) 的充要条件为:
考虑 DP 检查二分量 \(mid\) 是否合法。
具体地,先将特殊串 \(t_i\) 的权值设为 \(\log v_i - mid\),更新 ACAM 上各状态的权值,之后在 ACAM 上模拟匹配过程套路 DP。
设 \(f_{i,j}\) 表示长度为 \(i\),在 ACAM 上匹配的结束状态为 \(j\) 的串的最大价值。
初始化 \(f_{0,0} = 0\),转移时枚举串长,状态,转移函数。注意某一位不为\(\texttt{.}\)时转移函数只能为串中的字符,则有:
注意记录转移时的前驱与转移函数,根据前驱还原出方案即可。
总复杂度 \(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;
}

浙公网安备 33010602011771号