模拟费用流
模拟费用流
费用流常用结论:
- 费用流的凸性:增广路的长度会不断增长,费用是关于流量的凸函数。
- 需要考虑的负环(或增广路)不会多次经过同一个点(否则可以拆出一个环,正环不优,负环可以下次考虑)。
- 若每次消去权和最小的负环,则不存在一对被消去的负环经过一条边的方向相反(否则可以合并)。
常见模型:
- 最大流模型:无费用,也可以转化为最小割分析。
- 费用流增广模型:每次增广最短路,当单次费用为正时停止(任意流),或每次强制增广最短路(最大流)。
- 增量费用任意流模型:不断加点或边,只考虑所有新的负增广路与负环的贡献。
- 任意流消圈模型:求出任意一组最大流,然后消负环。
联系:反悔贪心中的反悔操作实际就是模拟费用流中的退流操作。
最大流模型
CF1368H2 Breadboard Capacity (hard version)
给出一张 \(n \times m\) 的电路板,边界有 \(2(n + m)\) 个接口,颜色为红色或蓝色,内部有 \(n \times m\) 个节点。
定义一条合法的电线连接当前仅当其满足:
- 两端点为异色接口。
- 除了在节点处可以拐弯,任何部分电线必须时水平或竖直的。
- 除了在节点处可以相交,其他部分不能与别的电线相交。
求能放置的最大电线数(H1)。
有 \(q\) 次修改,每次反转一段边界接口的颜色,并求能放置的最大电线数(H2)。
\(n, m, q \leq 10^5\)
先不考虑修改,考虑建立网络流模型:
- 源点连红接口,流量为 \(1\) 。
- 蓝接口连汇点,流量为 \(1\) 。
- 内部节点向四周连容量为 \(1\) 的无向边。
最大流即为答案。
考虑观察性质,最大流是不好观察的,考虑最小割,则确定了 \(n \times m\) 个节点的颜色后割即为异色点之间的边数。可以观察到:
- 存在一组最优解,使得每条割边都不成环。
- 证明:翻转环中点的颜色显然不劣。
- 存在一组最优解,使得割边形成的路径不连接同一边界或相邻边界。
- 证明:翻转内部点的颜色显然不劣。
- 存在一组最优解,使得任何一条割路径不转弯。
- 证明:翻转弯里面的点的颜色显然不劣。
因此存在一组最优解,使得割边形成的路径全为水平或全为竖直。
设 \(f_{i, 0/1}\) 表示考虑了前 \(i\) 列,第 \(i\) 列染红/蓝的最小割,转移不难做到线性,需要横竖都做一遍,即可通过 H1。
注意到 \(f\) 的转移可以写成矩阵的形式,线段树维护每个点是否翻转的矩阵,时间复杂度 \(O(n \log n + m \log m)\) 。
#include <bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e5 + 7;
char L[N], R[N], U[N], D[N];
int n, m, q;
struct SGT {
int s[N << 2], len[N << 2], tag[N << 2];
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
inline void spread(int x) {
s[x] = len[x] - s[x], tag[x] ^= 1;
}
inline void pushdown(int x) {
if (tag[x])
spread(ls(x)), spread(rs(x)), tag[x] = 0;
}
void build(int x, int l, int r, char *str) {
len[x] = r - l + 1, tag[x] = 0;
if (l == r) {
s[x] = (str[l] == 'B');
return;
}
int mid = (l + r) >> 1;
build(ls(x), l, mid, str), build(rs(x), mid + 1, r, str);
s[x] = s[ls(x)] + s[rs(x)];
}
void update(int x, int nl, int nr, int l, int r) {
if (l <= nl && nr <= r) {
spread(x);
return;
}
pushdown(x);
int mid = (nl + nr) >> 1;
if (l <= mid)
update(ls(x), nl, mid, l, r);
if (r > mid)
update(rs(x), mid + 1, nr, l, r);
s[x] = s[ls(x)] + s[rs(x)];
}
};
struct Matrix {
int a[2][2];
inline Matrix() {
memset(a, inf, sizeof(a));
}
inline Matrix operator * (const Matrix &rhs) const {
Matrix res;
for (int i = 0; i < 2; ++i)
for (int j = 0; j < 2; ++j)
for (int k = 0; k < 2; ++k)
res.a[i][k] = min(res.a[i][k], a[i][j] + rhs.a[j][k]);
return res;
}
};
struct SMT {
Matrix s[N << 2][4];
int tag[N << 2];
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
inline void pushup(int x) {
for (int i = 0; i < 4; ++i)
s[x][i] = s[ls(x)][i] * s[rs(x)][i];
}
inline void spread(int x, int k) {
vector<Matrix> vec(s[x], s[x] + 4);
for (int i = 0; i < 4; ++i)
s[x][i ^ k] = vec[i];
tag[x] ^= k;
}
inline void pushdown(int x) {
if (tag[x])
spread(ls(x), tag[x]), spread(rs(x), tag[x]), tag[x] = 0;
}
void build(int x, int l, int r, char *a, char *b, int n) {
tag[x] = 0;
if (l == r) {
for (int i = 0; i < 4; ++i) {
int k = ((a[l] == 'B') ^ (i >> 1 & 1)) + ((b[l] == 'B') ^ (i & 1));
s[x][i].a[0][0] = k, s[x][i].a[0][1] = n + 2 - k;
s[x][i].a[1][0] = n + k, s[x][i].a[1][1] = 2 - k;
}
return;
}
int mid = (l + r) >> 1;
build(ls(x), l, mid, a, b, n), build(rs(x), mid + 1, r, a, b, n);
pushup(x);
}
void update(int x, int nl, int nr, int l, int r, int k) {
if (l <= nl && nr <= r) {
spread(x, k);
return;
}
pushdown(x);
int mid = (nl + nr) >> 1;
if (l <= mid)
update(ls(x), nl, mid, l, r, k);
if (r > mid)
update(rs(x), mid + 1, nr, l, r, k);
pushup(x);
}
};
struct Solver {
SGT L, R;
SMT smt;
int n, m;
inline void prework(int _n, int _m, char *_L, char *_R, char *_U, char *_D) {
n = _n, m = _m, L.build(1, 1, n, _L), R.build(1, 1, n, _R), smt.build(1, 1, m, _U, _D, n);
}
inline void flipL(int l, int r) {
L.update(1, 1, n, l, r);
}
inline void flipR(int l, int r) {
R.update(1, 1, n, l, r);
}
inline void flipU(int l, int r) {
smt.update(1, 1, m, l, r, 2);
}
inline void flipD(int l, int r) {
smt.update(1, 1, m, l, r, 1);
}
inline int query() {
Matrix f;
f.a[0][0] = L.s[1], f.a[0][1] = n - L.s[1];
f = f * smt.s[1][0];
return min(f.a[0][0] + R.s[1], f.a[0][1] + n - R.s[1]);
}
} solver1, solver2;
signed main() {
scanf("%d%d%d%s%s%s%s", &n, &m, &q, L + 1, R + 1, U + 1, D + 1);
solver1.prework(n, m, L, R, U, D), solver2.prework(m, n, U, D, L, R);
printf("%d\n", min(solver1.query(), solver2.query()));
while (q--) {
char op[2];
int l, r;
scanf("%s%d%d", op, &l, &r);
if (op[0] == 'L')
solver1.flipL(l, r), solver2.flipU(l, r);
else if (op[0] == 'R')
solver1.flipR(l, r), solver2.flipD(l, r);
else if (op[0] == 'U')
solver1.flipU(l, r), solver2.flipL(l, r);
else
solver1.flipD(l, r), solver2.flipR(l, r);
printf("%d\n", min(solver1.query(), solver2.query()));
}
return 0;
}
CF1895G Two Characters, Two Colors
给定 01 串 \(s_{1 \sim n}\) ,每个位置保留则获得 \(r_i\) 的收益,删去则获得 \(b_i\) 的收益,最大化收益和减去剩余元素的逆序对数。
\(n \leq 4 \times 10^5\)
对于一对逆序对,将其视为同时保留会产生 \(-1\) 的贡献。由于是 01 串,于是一个很好的性质是负贡献一定在一对 \(0, 1\) 之间产生。
先令总收益为 \(\sum r_i + b_i\) ,则需要最小化负收益,考虑建立最小割模型:
- 对每个 \(s_i = 0\) 的位置,连边 \((S, i, b_i), (i, T, r_i)\) 。
- 对每个 \(s_i = 1\) 的位置,连边 \((S, i, r_i), (i, T, b_i)\) 。
- 对每个 \(s_i = 1\) 的位置,向后面 \(s_j = 0\) 的位置连 \(i \to j\) 流量为 \(1\) 的边。
最小割即为负收益。
接下来考虑模拟最大流,显然有流量下界 \(\sum \min(r_i, b_i)\) ,然后考虑剩余的流量:
- 对于一个 \(1\) ,若 \(r_i > b_i\) ,则还有 \(r_i - b_i\) 的流量可以流到后面的 \(0\) 。
- 对于一个 \(0\) ,若 \(r_i > b_i\) ,则还能接受来自前面 \(1\) 的 \(r_i - b_i\) 的流量。
于是可以倒序枚举位置,每次遇到一个 \(r_i > b_i\) 的 \(0\) 就把 \(r_i - b_i\) 加入集合,遇到一个 \(r_i > b_i\) 的 \(1\) 就把集合前 \(r_i - b_i\) 大元素减去 \(1\) ,然后把减到 \(0\) 的元素删掉即可。
使用 fhq-Treap 维护即可做到 \(O(n \log n)\) 。
#include <bits/stdc++.h>
typedef unsigned int uint;
typedef long long ll;
using namespace std;
const int N = 4e5 + 7;
ll r[N], b[N];
char str[N];
int n;
namespace fhqTreap {
ll val[N];
uint dat[N];
int lc[N], rc[N], siz[N], tag[N];
mt19937 myrand(time(0));
int tot, root;
inline int newnode(ll k) {
dat[++tot] = myrand(), val[tot] = k, lc[tot] = rc[tot] = tag[tot] = 0, siz[tot] = 1;
return tot;
}
inline void spread(int x, int k) {
val[x] += k, tag[x] += k;
}
inline void pushdown(int x) {
if (tag[x]) {
if (lc[x])
spread(lc[x], tag[x]);
if (rc[x])
spread(rc[x], tag[x]);
tag[x] = 0;
}
}
inline void pushup(int x) {
siz[x] = siz[lc[x]] + siz[rc[x]] + 1;
}
void split_siz(int x, int k, int &a, int &b) {
if (!x) {
a = b = 0;
return;
}
pushdown(x);
if (siz[lc[x]] + 1 <= k)
a = x, split_siz(rc[x], k - siz[lc[x]] - 1, rc[a], b);
else
b = x, split_siz(lc[x], k, a, lc[b]);
pushup(x);
}
void split_val(int x, ll k, int &a, int &b) {
if (!x) {
a = b = 0;
return;
}
pushdown(x);
if (val[x] <= k)
a = x, split_val(rc[x], k, rc[a], b);
else
b = x, split_val(lc[x], k, a, lc[b]);
pushup(x);
}
int merge(int a, int b) {
if (!a || !b)
return a | b;
pushdown(a), pushdown(b);
if (dat[a] > dat[b])
return rc[a] = merge(rc[a], b), pushup(a), a;
else
return lc[b] = merge(a, lc[b]), pushup(b), b;
}
inline void insert(ll k) {
int a, b;
split_val(root, k, a, b);
root = merge(merge(a, newnode(k)), b);
}
inline ll querymin(int x) {
while (lc[x])
pushdown(x), x = lc[x];
return val[x];
}
inline ll querymax(int x) {
while (rc[x])
pushdown(x), x = rc[x];
return val[x];
}
inline void update(int k) {
int a, b;
split_siz(root, siz[root] - k, a, b);
if (querymax(a) < querymin(b))
spread(b, -1), root = merge(a, b);
else {
ll v = querymax(a);
int c, d;
split_val(a, v - 1, a, c), split_val(b, v, b, d);
spread(b, -1), spread(d, -1);
root = merge(merge(a, b), merge(c, d));
}
split_val(root, 0, a, root);
}
} // namespace fhqTreap
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%s", &n, str + 1);
for (int i = 1; i <= n; ++i)
scanf("%lld", r + i);
for (int i = 1; i <= n; ++i)
scanf("%lld", b + i);
ll ans = 0;
for (int i = 1; i <= n; ++i)
ans += max(r[i], b[i]); // r + b - min(r, b) = max(r, b)
fhqTreap::root = fhqTreap::tot = 0;
for (int i = n; i; --i) {
if (r[i] <= b[i])
continue;
if (str[i] == '0')
fhqTreap::insert(r[i] - b[i]);
else {
int k = min((ll)fhqTreap::siz[fhqTreap::root], r[i] - b[i]);
ans -= k, fhqTreap::update(k);
}
}
printf("%lld\n", ans);
}
return 0;
}
CF1408H Rainbow Triples
给定序列 \(p_{1 \sim n}\) ,选出若干三元组 \((a_i, b_i, c_i)\) 满足:
- \(1 \leq a_i < b_i < c_i \leq n\) 。
- \(a_i = c_i = 0\) ,\(b_i \neq 0\) 。
- \(p_{b_i}\) 互不相同。
- 所有 \(a_i, b_i, c_i\) 互不相同。
最大化选出的三元组数量。
\(n \leq 5 \times 10^5\)
记 \(m\) 为 \(0\) 的数量,显然答案 \(\leq \lfloor \frac{m}{2} \rfloor\) 。考虑将所有 \(0\) 均分为左右两段,则不难发现存在一组最优解满足 \(a_i\) 为左半边的 \(0\) 且 \(c_i\) 为右半边的 \(0\) ,这可以通过调整法证明。
进一步分析可以得到,若 \(b_i\) 在左半边,则右半边一定可以匹配,因此只要考虑 \(a_i\) 的选取即可,同理 \(b_i\) 在右半边时只要考虑 \(c_i\) 的选取即可。因此对于每个 \(p_i \neq 0\) 的位置只要在左右各保留一个即可,贪心保留最靠近中心的。
考虑网络流建模:
- \(S\) 向每个非 \(0\) 值对应点连边,边权为 \(1\) 。
- 对位置建点 \(1 \sim n\) ,左半边连成递减的链,右半边连成递增的链(即中心向两边的方向),边权均为 \(+ \infty\) 。
- 每个非 \(0\) 值对应点向对应的左右两个位置连边(若存在),边权为 \(1\) 。
- 将所有 \(0\) 位置向 \(T\) 连边,边权为 \(1\) 。
最大流即为答案,数据范围启发我们考虑模拟费用流。
尝试最小割模型,若某颜色的边不割,则必须割掉一段前缀 \(0\) 与一段后缀 \(0\) ,因此最小割一定形如:一段前缀 \(0\) 、一段后缀 \(0\) 、一部分颜色。
考虑枚举后缀 \(0\) 的割掉的位置,用线段树维护一段前缀 \(0\) 和一部分颜色的割边数量即可,时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 7;
int a[N], pre[N], suf[N], L[N], R[N];
int n;
namespace SMT {
int mn[N << 2], tag[N << 2];
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
inline void spread(int x, int k) {
mn[x] += k, tag[x] += k;
}
inline void pushdown(int x) {
if (tag[x])
spread(ls(x), tag[x]), spread(rs(x), tag[x]), tag[x] = 0;
}
void build(int x, int l, int r) {
tag[x] = 0;
if (l == r) {
mn[x] = l;
return;
}
int mid = (l + r) >> 1;
build(ls(x), l, mid), build(rs(x), mid + 1, r);
mn[x] = min(mn[ls(x)], mn[rs(x)]);
}
void update(int x, int nl, int nr, int l, int r, int k) {
if (l <= nl && nr <= r) {
spread(x, k);
return;
}
pushdown(x);
int mid = (nl + nr) >> 1;
if (l <= mid)
update(ls(x), nl, mid, l, r, k);
if (r > mid)
update(rs(x), mid + 1, nr, l, r, k);
mn[x] = min(mn[ls(x)], mn[rs(x)]);
}
} // namespace SMT
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d", &n);
set<int> st;
for (int i = 1; i <= n; ++i)
scanf("%d", a + i), st.emplace(a[i]);
st.erase(0), pre[0] = 0;
for (int i = 1; i <= n; ++i)
pre[i] = pre[i - 1] + !a[i];
suf[n + 1] = 0;
for (int i = n; i; --i)
suf[i] = suf[i + 1] + !a[i];
int m = pre[n] / 2, mid = upper_bound(pre + 1, pre + n + 1, m) - pre - 1;
memset(L + 1, 0, sizeof(int) * n), memset(R + 1, 0, sizeof(int) * n);
for (int i = 1; i <= mid; ++i)
L[a[i]] = i;
for (int i = n; i > mid; --i)
R[a[i]] = i;
int ans = min((int)st.size(), m);
SMT::build(1, 0, m), SMT::spread(1, st.size());
for (int it : st)
if (!R[it])
SMT::update(1, 0, m, pre[L[it]], m, -1), ans = min(ans, SMT::mn[1]);
for (int i = n; i > mid; --i)
if (a[i] && R[a[i]] == i)
SMT::update(1, 0, m, pre[L[a[i]]], m, -1), ans = min(ans, SMT::mn[1] + suf[i]);
printf("%d\n", ans);
}
return 0;
}
费用流增广模型
P1484 种树
求 \(n\) 个数中选出至多 \(k\) 个两两不相邻的数的最大和。
\(n \leq 3 \times 10^5\)
对 \(n - 1\) 个间隔建点,则选一个位置可以转化为相邻间隔的匹配,建立费用流模型:
- \(S\) 向奇数间隔连流量为 \(1\) ,费用为 \(0\) 的边。
- 偶数间隔向 \(T\) 连流量为 \(1\) ,费用为 \(0\) 的边。
- 奇数间隔向相邻偶数间隔连流量为 \(1\) ,费用为两个间隔之间的数的边。
流量限制为 \(k\) ,最大费用任意流即为答案。
由于费用流的凸性,因此可以 wqs 二分配合 DP 解决,这并不重要。
考虑一次增广,会流过若干奇偶间隔之间的连续边,其表示将这一段的状态都取反,即将一段 0101...010 变为 1010...101 。
用堆维护这样选取状态交替改变的连续段,设当前选出的最大的点为 \(x\) ,它左右两边的点分别是 \(y, z\) ,就删掉这三个点并新建一个点权为 \(a_y + a_z - a_x\) 的,这样下次选出这个点时就实现了退流操作,用链表维护相邻点即可。
时间复杂度 \(O(n \log n)\) 。
类似的题目:
- P1792 [国家集训队] 种树 :推广到环上,注意这题是选恰好 \(m\) 个。
- P3620 [APIO/CTSC2007] 数据备份 :贪心可以感受到二元组一定相邻,将相邻两个数建点则转化为这题。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 5e5 + 7;
struct List {
int pre, nxt;
ll val;
} a[N];
priority_queue<pair<ll, int> > q;
bool vis[N];
int n, k;
signed main() {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i) {
scanf("%lld", &a[i].val), q.emplace(a[i].val, i);
a[i].pre = i - 1, a[i].nxt = i + 1;
}
ll ans = 0;
while (k--) {
while (vis[q.top().second])
q.pop();
ll res = q.top().first;
int x = q.top().second, y = a[x].pre, z = a[x].nxt;
q.pop();
if (res <= 0)
break;
ans += res, vis[y] = vis[z] = true;
a[x].val = a[y].val + a[z].val - a[x].val;
a[a[x].pre = a[y].pre].nxt = a[a[x].nxt = a[z].nxt].pre = x;
q.emplace(a[x].val, x);
}
printf("%lld", ans);
return 0;
}
P5470 [NOI2019] 序列
给定 \(a_{1 \sim n}, b_{1 \sim n}\) ,需要分别对两个序列各指定恰好 \(k\) 个下标,要求至少有 \(l\) 个下标在两个序列中都被指定,最大化这 \(2k\) 个下标在序列中对应的元素的总和。
多测,\(\sum n \leq 10^6\)
考虑费用流建模。至少 \(l\) 个下标相等是不好用流量表示限制的,考虑限制至多 \(k - l\) 个下标不相等,这可以通过建立虚点 \(C, D\) 实现。
- \(S\) 向每个 \(a_i\) 代表的点连流量为 \(1\) 、费用为 \(a_i\) 的边。
- 每个 \(b_i\) 代表的点向 \(T\) 连流量为 \(1\) 、费用为 \(b_i\) 的边。
- 每个 \(a_i\) 代表向每个 \(b_i\) 代表的点连流量为 \(1\) 、费用为 \(0\) 的边。
- 每个 \(a_i\) 代表向 \(C\) 连流量为 \(1\) 、费用为 \(0\) 的边。
- \(C\) 向 \(D\) 连流量为 \(k - l\) 、费用为 \(0\) 的边。
- \(D\) 向每个 \(b_i\) 代表的点连流量为 \(1\) 、费用为 \(0\) 的边。
源点流量限制为 \(k\) ,最大费用流即为答案。
考虑对 \(k\) 个流量依次找增广路:
- \(S \to A_i \to B_i \to T\) :匹配 \((a_i, b_i)\) 。
- \(S \to A_i \to C \to D \to B_j \to T\) :匹配 \((a_ i, b_j)\) ,\(C \to D\) 流量减少 \(1\) 。
- \(S \to A_i \to B_i \to D \to C \to A_j \to B_j \to T\) :将 \((a_j, b_i)\) 调整为 \((a_i, b_i), (a_j, b_j)\) ,\(C \to D\) 流量增加 \(1\) 。
- \(S \to A_i \to B_i \to D \to B_k \to T\) :将 \((a_j, b_i)\) 调整为 \((a_i, b_i), (a_j, b_k)\) 。
- \(S \to A_i \to C \to A_k \to B_k \to T\) :将 \((a_k, b_j)\) 调整为 \((a_i, b_j), (a_k, b_k)\) 。
权值均为最大值时优先选 \(C \to D\) 流量增加的方案,其次选 \(C \to D\) 流量不变的方案,最后选 \(C \to D\) 流量减少的方案。这是因为当出现 \((a_i, b_j), (a_j, b_i)\) 时显然调整为 \((a_i, b_i), (a_j, b_j)\) 是更优的。
用堆维护,时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e5 + 7;
int tag[N];
struct Heap {
priority_queue<pair<int, int> > q;
int op1, op2;
inline Heap(int _op1 = -1, int _op2 = -1) : op1(_op1), op2(_op2) {}
inline void clear() {
while (!q.empty())
q.pop();
}
inline void maintain() {
while (!q.empty() && tag[q.top().second] != op1 && tag[q.top().second] != op2)
q.pop();
}
inline bool empty() {
return maintain(), q.empty();
}
inline pair<int, int> top() {
return maintain(), q.top();
}
inline void emplace(int k, int x) {
q.emplace(k, x);
}
} qa(0, 2), qb(0, 1), q(0), qab(2), qba(1);
int a[N], b[N];
int n, k, m;
inline void update(int x) {
if (!tag[x])
qa.emplace(a[x], x), qb.emplace(b[x], x), q.emplace(a[x] + b[x], x);
else if (tag[x] == 1)
qb.emplace(b[x], x), qba.emplace(b[x], x);
else if (tag[x] == 2)
qa.emplace(a[x], x), qab.emplace(a[x], x);
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d%d", &n, &k, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
for (int i = 1; i <= n; ++i)
scanf("%d", b + i);
qa.clear(), qb.clear(), q.clear(), qab.clear(), qba.clear();
for (int i = 1; i <= n; ++i)
tag[i] = 0, update(i);
m = k - m;
ll ans = 0;
while (k--) {
int res1 = -inf, res2 = -inf, res3 = -inf, res4 = -inf, res5 = -inf;
if (!q.empty())
res1 = q.top().first;
if (m && !qa.empty() && !qb.empty())
res2 = qa.top().first + qb.top().first;
if (!qab.empty() && !qba.empty())
res3 = qab.top().first + qba.top().first;
if (!qab.empty() && !qb.empty())
res4 = qab.top().first + qb.top().first;
if (!qa.empty() && !qba.empty())
res5 = qa.top().first + qba.top().first;
int res = max({res1, res2, res3, res4, res5});
ans += res;
if (res3 == res) {
int x = qab.top().second, y = qba.top().second;
++m, tag[x] |= 1, update(x), tag[y] |= 2, update(y);
} else if (res1 == res) {
int x = q.top().second;
tag[x] = 3, update(x);
} else if (res4 == res) {
int x = qab.top().second, y = qb.top().second;
tag[x] |= 1, update(x), tag[y] |= 2, update(y);
} else if (res5 == res) {
int x = qa.top().second, y = qba.top().second;
tag[x] |= 1, update(x), tag[y] |= 2, update(y);
} else {
int x = qa.top().second, y = qb.top().second;
--m, tag[x] |= 1, update(x), tag[y] |= 2, update(y);
}
}
printf("%lld\n", ans);
}
return 0;
}
Pretty Boxes
有 \(n\) 个箱子,每个箱子有大小 \(s_i\) 和价值 \(p_i\) 。
定义一对箱子 \((a, b)\) 的价值为:
- \(s_a > s_b\) :获得 \(p_a - p_b\) 的价值。
- \(s_a <s_b\) :获得 \(p_b - p_a\) 的价值。
- \(s_a = s_b\) :获得 \(|p_a - p_b|\) 的价值。
对于 \(k = 1, 2, \cdots, \lfloor \frac{n}{2} \rfloor\) ,求至多选 \(k\) 对互不相同(需要选 \(2k\) 个互不相同的箱子)的箱子的最大价值和。
\(n \leq 2 \times 10^5\)
考虑将所有箱子按 \(s\) 为第一关键字、\(p\) 为第二关键字降序排序,则问题转化为选出 \(k\) 对括号匹配,其中左括号处权值为正,右括号处权值为负。
由于是至多选 \(k\) 对,因此考虑钦定自己可以和自己匹配,这样没有贡献,问题转化为恰好选 \(k\) 对。
考虑建立费用流模型,除了 \(1 \sim n\) 和 \(S, T\) 外额外建 \(n\) 个后缀点 \(n + 1 \sim n + n\) :
- \(S\) 向 \(i = 1, 2, \cdots, n\) 连边,流量为 \(1\) ,费用为 \(p_i\) :表示该点可以作为一个左括号。
- \(i = 1, 2, \cdots, n\) 向 \(T\) 连边,流量为 \(1\) ,费用为 \(-p_i\) :表示该点可以作为一个右括号。
- \(i\) 向 \(n + i\) 连边,流量为 \(1\) ,费用为 \(0\) :表示该点可以向后匹配。
- \(n + i\) 向 \(n + i + 1\) 连边,流量为 \(+ \infty\) ,费用为 \(0\) :表示该点可以继承前面的左括号。
- \(n + i\) 向 \(i\) 连边:表示该点可以接受前面的左括号。
每次分配 \(1\) 的流量跑,最大费用最大流即为答案。考虑优化,观察到增广路只有两种形式:
- 选择两个未匹配点 \(a < b\) 匹配,价值为 \(p_a - p_b\) 。
- 选择两个未匹配点 \(a < b\) 匹配,价值为 \(p_b - p_a\) ,要求 \(b\) 能通过退流边到达 \(a\) 。
第一个情况不难用线段树处理,考虑表示第二个情况的限制。
对于 \(n - 1\) 条后缀点的边,记 \(c_i\) 表示该边可以被退流几次,则选出一个增广路之后相当于区间 \(\pm 1\) 。限制条件即为 \(\min_{i = a}^{b - 1} c_i > 0\) ,即按 \(c_i > 0\) 的部分分段后每段内部可以选。
但是直接维护 \(c_i > 0\) 的段不好做区间修改,由于 \(c_i \geq 0\) ,考虑引入 \(c_n= 0\) ,并将分段方式改为 \(c_i > \min_{j = L}^R c_j\) ,这样就可以比较方便的合并区间信息了。
时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2e5 + 7;
pair<int, int> a[N];
int n;
namespace SMT {
struct Node {
tuple<int, int, int> ans1, ans2, ans3;
pair<int, int> mn, mx, mnl, mnr, mxl, mxr;
int mnc;
// ans1 : 第一种匹配
// ans2 : 第二种匹配,并考虑区间最小值的限制
// ans3 : 第二种匹配,不考虑区间最小值的限制
// mn, mx : 区间 p 的极值
// mnl, mnr, mxl, mxr : 区间 p 的极值,并考虑区间最小值的限制
// mnc : 区间最小退流边的可退流量
inline friend Node operator + (const Node &a, const Node &b) {
Node c;
c.mnc = min(a.mnc, b.mnc), c.mn = min(a.mn, b.mn), c.mx = max(a.mx, b.mx);
c.ans1 = max(max(a.ans1, b.ans1), make_tuple(a.mx.first - b.mn.first, a.mx.second, b.mn.second));
c.ans3 = max(max(a.ans3, b.ans3), make_tuple(b.mx.first - a.mn.first, a.mn.second, b.mx.second));
if (a.mnc == b.mnc) {
c.mnl = a.mnl, c.mnr = b.mnr, c.mxl = a.mxl, c.mxr = b.mxr;
c.ans2 = max(max(a.ans2, b.ans2), make_tuple(b.mxl.first - a.mnr.first, a.mnr.second, b.mxl.second));
} else if (a.mnc < b.mnc) {
c.mnl = a.mnl, c.mnr = min(a.mnr, b.mn), c.mxl = a.mxl, c.mxr = max(a.mxr, b.mx);
c.ans2 = max(max(a.ans2, b.ans3), make_tuple(b.mx.first - a.mnr.first, a.mnr.second, b.mx.second));
} else {
c.mnl = min(a.mn, b.mnl), c.mnr = b.mnr, c.mxl = max(a.mx, b.mxl), c.mxr = b.mxr;
c.ans2 = max(max(a.ans3, b.ans2), make_tuple(b.mxl.first - a.mn.first, a.mn.second, b.mxl.second));
}
return c;
}
} nd[N << 2];
int tag[N << 2];
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
inline void spread(int x, int k) {
nd[x].mnc += k, tag[x] += k;
}
inline void pushdown(int x) {
if (tag[x])
spread(ls(x), tag[x]), spread(rs(x), tag[x]), tag[x] = 0;
}
void build(int x, int l, int r) {
if (l == r) {
nd[x].ans1 = nd[x].ans2 = nd[x].ans3 = make_tuple(0, l, l);
nd[x].mnc = 0, nd[x].mn = nd[x].mx = make_pair(a[l].second, l);
nd[x].mnl = nd[x].mnr = make_pair(inf, 0), nd[x].mxl = nd[x].mxr = make_pair(-inf, 0);
return;
}
int mid = (l + r) >> 1;
build(ls(x), l, mid), build(rs(x), mid + 1, r);
nd[x] = nd[ls(x)] + nd[rs(x)];
}
void update(int x, int nl, int nr, int l, int r, int k) {
if (l <= nl && nr <= r) {
spread(x, k);
return;
}
pushdown(x);
int mid = (nl + nr) >> 1;
if (l <= mid)
update(ls(x), nl, mid, l, r, k);
if (r > mid)
update(rs(x), mid + 1, nr, l, r, k);
nd[x] = nd[ls(x)] + nd[rs(x)];
}
void modify(int x, int nl, int nr, int p) {
if (nl == nr) {
nd[x].ans1 = nd[x].ans2 = nd[x].ans3 = make_tuple(-inf, 0, 0);
nd[x].mn = nd[x].mnl = nd[x].mnr = make_pair(inf, 0);
nd[x].mx = nd[x].mxl = nd[x].mxr = make_pair(-inf, 0);
return;
}
pushdown(x);
int mid = (nl + nr) >> 1;
if (p <= mid)
modify(ls(x), nl, mid, p);
else
modify(rs(x), mid + 1, nr, p);
nd[x] = nd[ls(x)] + nd[rs(x)];
}
} // namespace SMT
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d%d", &a[i].first, &a[i].second);
sort(a + 1, a + n + 1, greater<pair<int, int> >());
SMT::build(1, 1, n);
ll ans = 0;
for (int i = 1; i <= n / 2; ++i) {
auto res = max(SMT::nd[1].ans1, SMT::nd[1].ans2);
printf("%lld\n", ans += get<0>(res));
if (get<1>(res) < get<2>(res))
SMT::update(1, 1, n, get<1>(res), get<2>(res) - 1, res == SMT::nd[1].ans1 ? 1 : -1);
SMT::modify(1, 1, n, get<1>(res)), SMT::modify(1, 1, n, get<2>(res));
}
return 0;
}
CF2029I Variance Challenge
给出 \(a_{1 \sim n}\) 与整数 \(k\) ,定义一次操作为选择一个区间加 \(k\) 。对于 \(p = 1, 2, \cdots, m\) ,求 \(p\) 次操作后 \(a\) 的方差乘 \(n^2\) 的最小值。
\(n, m \leq 5000\) ,\(nm \leq 2 \times 10^4\)
注意到函数 \(f(x) = \sum (a_i - x)^2\) 的最小值在 \(\overline{a}\) 处取到,为 \(a\) 的方差乘上 \(n\) 。
可以发现可能的 \(\overline{a}\) 只有 \(nm\) 种,考虑枚举 \(\overline{a} = a_0\) 计算 \(1 \leq p \leq m\) 时 \(f(x_0)\) 的最小值。
建立费用流模型:
- \(S\) 向 \(i\) 连流量为 \(+ \infty\) 、费用为 \(0\) 的边,表示区间开始。
- \(i\) 向 \(T\) 连流量为 \(+ \infty\) 、费用为 \(0\) 的边,表示区间结束。
- \(i\) 向 \(i + 1\) 连 \(m\) 条边,流量均为 \(1\) ,第 \(j\) 条边费用为 \((a_i + jk - x_0)^2 - (a_i + (j - 1)k - x_0)^2\) 。
- 正确性:由于 \(f(x)\) 是下凸的,因此导数递增,每次都会优先流费用最小的边。
限制流量为 \(m\) ,直接模拟增广的流程,维护正向边和反向边,每次暴力遍历选出最小子段和即可,时间复杂度 \(O(n^2 m^2)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 5e3 + 7;
__int128 a[N], ans[N];
int f[N];
__int128 k;
int n, m;
inline void write(__int128 x) {
if (x < inf)
printf("%lld ", (ll)x);
else
printf("%lld%018lld ", (ll)(x / inf), (ll)(x % inf));
}
signed main() {
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d%d", &n, &m, &k), k *= n;
__int128 S = 0;
for (int i = 1; i <= n; ++i)
scanf("%d", a + i), S += (a[i] *= n);
fill(ans + 1, ans + m + 1, (__int128)inf * inf);
for (int t = 1; t <= n * m; ++t) {
__int128 average = (S + k * t) / n, res = 0;
auto sq = [](__int128 x) {
return x * x;
};
for (int i = 1; i <= n; ++i)
res += sq(a[i] - average);
memset(f + 1, 0, sizeof(int) * n);
for (int p = 1; p <= m; ++p) {
__int128 mx = -inf, sum = 0;
int L = 0, R = 0, op = 0;
for (int i = 1, j = 1; i <= n; ++i) {
sum += sq(a[i] + f[i] * k - average) - sq(a[i] + k * (f[i] + 1) - average);
if (sum > mx)
mx = sum, op = 1, L = j, R = i;
if (sum < 0)
sum = 0, j = i + 1;
}
sum = 0;
for (int i = 1, j = 1; i <= n; ++i) {
sum += f[i] ? sq(a[i] + f[i] * k - average) - sq(a[i] + k * (f[i] - 1) - average) : -inf;
if (sum > mx)
mx = sum, op = -1, L = j, R = i;
if (sum < 0)
sum = 0, j = i + 1;
}
res -= mx;
for (int i = L; i <= R; ++i)
f[i] += op;
ans[p] = min(ans[p], res);
}
}
for (int i = 1; i <= m; ++i)
write(ans[i] / n);
puts("");
}
return 0;
}
增量费用任意流模型
P4694 [PA 2013] Raper
需要生产 \(k\) 张光盘,每张光盘都进行先 A 厂、后 B 厂的加工。一共有 \(n\) 天,第 \(i\) 天两者加工费用分别为 \(a_i, b_i\) 。
每天可以先送一张光盘到 A 厂(或者不送),再送一张在 A 厂加工过的光盘到 B 厂(或者不送)。
每家工厂一天只能对一张光盘进行操作,同一张光盘在一天内生产出来是允许的。
求生产 \(k\) 张光盘的最小花费。
\(k \leq n \leq 5 \times 10^5\)
建立费用流模型:
- \(S\) 向每天的 A 厂连边,流量为 \(1\) ,费用为 \(a_i\) 。
- 每天的 B 厂向 \(T\) 连边,流量为 \(1\) ,费用为 \(b_i\) 。
- 对于 \(i \leq j\) ,第 \(i\) 天的 A 厂向第 \(j\) 天的 B 厂连边,流量为 \(1\) ,费用为 \(0\) 。
- 限制源点总流量为 \(k\) 。
由费用流的凸性,答案关于 \(k\) 是凸的,因此可以 wqs 二分,则无需考虑 \(k\) 的限制。
考虑增量最小费用任意流模型,按时间倒序加点,则每个加入的 A 都能与原来的 B 匹配,此时会出现:
-
新的负增广路: \(S \to A \to B \to T\) ,对应当前 \(A\) 选一个最小的未匹配的 \(B\) 。
-
新的负环:\(S \to A \to B \to A' \to S\) :对应当前 \(A\) 去替换之前一个较大的 \(A'\)。
用堆维护新的负环即可做到 \(O(n \log n \log V)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 0x3f3f3f3f3f3f3f3f;
const int N = 5e5 + 7;
ll a[N], b[N];
int n, k;
inline pair<ll, int> check(ll lambda) {
priority_queue<ll> qa;
priority_queue<ll, vector<ll>, greater<ll> > qb;
pair<ll, int> ans = make_pair(0, 0);
for (int i = n; i; --i) {
qb.emplace(b[i]);
ll res1 = inf, res2 = inf;
if (!qa.empty())
res1 = a[i] - qa.top();
if (!qb.empty())
res2 = a[i] + qb.top() - lambda;
if (min(res1, res2) <= 0) {
if (res2 <= res1)
ans.first += res2, ++ans.second, qb.pop(), qa.emplace(a[i]);
else
ans.first += res1, qa.pop(), qa.emplace(a[i]);
}
}
return ans;
}
signed main() {
scanf("%d%d", &n, &k);
for (int i = 1; i <= n; ++i)
scanf("%lld", a + i);
for (int i = 1; i <= n; ++i)
scanf("%lld", b + i);
ll l = -1e14, r = 1e14, ans = r;
while (l <= r) {
ll mid = (l + r) >> 1;
if (check(mid).second >= k)
ans = mid, r = mid - 1;
else
l = mid + 1;
}
printf("%lld", check(ans).first + 1ll * ans * k);
return 0;
}
P6122 [NEERC 2016] Mole Tunnels
给出一棵满二叉树,其中 \(fa_i = \lfloor \frac{i}{2} \rfloor\) ,第 \(i\) 个点上有 \(c_i\) 个食物。有 \(m\) 只鼹鼠,第 \(i\) 只鼹鼠在 \(p_i\) 。对于所有 \(k \in [1, m]\) ,求前 \(k\) 只鼹鼠吃到食物的最小行动距离,其中每个食物只能供一只鼹鼠吃。
\(n, m \leq 10^5\)
建立费用流模型:
- \(S\) 向所有食物连流量为 \(c_i\) 、费用为 \(0\) 的边。
- 每个食物向相邻的食物连流量为 \(+ \infty\) 、费用为 \(1\) 的边。
- 每个 \(p_i\) 向 \(T\) 连流量为 \(1\) 、费用为 \(0\) 的边,出现多少次就连多少条。
考虑增量最小费用最大流,则每次需要找到一条最短的增广路。
对于一对父子关系 \((f, u)\) ,一开始的边是 \((u, f, + \infty, 1), (f, u, 0, -1), (f, u, + \infty, 1), (u, f, 0, -1)\) ,因此每次找增广路时优先走反向边。
考虑维护每个点的从上到下的流量 \(f_i\) ,当其为正时表示父亲到儿子只能走正向边,儿子到父亲优先选反向边,为负时相反,为 \(0\) 时都只能走正向边。再维护 \(g_i\) 表示 \(i\) 到子树内距离最短的点。
新加入点 \(p\) 时,不断跳父亲 \(u\) 找 \(p \to u \to g_u\) 最短的增广路,然后暴力向上更新即可。
时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 1e5 + 7;
pair<int, int> g[N];
int a[N], f[N];
int n, m;
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
inline void pushup(int x) {
g[x] = a[x] ? make_pair(0, x) : make_pair(inf, 0);
if (ls(x) <= n)
g[x] = min(g[x], make_pair(g[ls(x)].first + (f[ls(x)] < 0 ? -1 : 1), g[ls(x)].second));
if (rs(x) <= n)
g[x] = min(g[x], make_pair(g[rs(x)].first + (f[rs(x)] < 0 ? -1 : 1), g[rs(x)].second));
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
for (int i = n; i; --i)
pushup(i);
ll ans = 0;
while (m--) {
int x;
scanf("%d", &x);
pair<int, int> cur = make_pair(inf, 0);
for (int u = x, d = 0; u; d += (f[u] > 0 ? -1 : 1), u >>= 1)
cur = min(cur, make_pair(g[u].first + d, u));
printf("%lld ", ans += cur.first);
for (int u = x; u != cur.second; u >>= 1)
--f[u], pushup(u);
--a[g[cur.second].second];
for (int u = g[cur.second].second; u != cur.second; u >>= 1)
++f[u], pushup(u);
for (int u = cur.second; u; u >>= 1)
pushup(u);
}
return 0;
}
P3826 [NOI2017] 蔬菜
有 \(n\) 种蔬菜,第 \(i\) 种单价为 \(a_i\) ,初始数量为 \(c_i\) 。
初次出售第 \(i\) 种蔬菜时会获得 \(s_i\) 的奖励,若不出售则会有 \(x_i\) 数量会变质。
每天最多出售 \(m\) 数量蔬菜,\(k\) 次询问总天数为 \(p_i\) 时的最大收益。
\(n, p_i \leq 10^5\) ,\(m \leq 10\)
首先最优情况下每天一定优先出售当天将要变质的蔬菜,可以理解为按照过期时间将其分类。
根据经验,删除时不好处理的,考虑时光倒流转化为加入。每天都会进货一些菜,菜可以无限期保存。
由此可以建立费用流模型,对第 \(j\) 天进货的第 \(i\) 种蔬菜建点 \((i, j)\) ,对第 \(i\) 天出售建点 \(h_i\) 。
- \(S \to (i, j)\) ,流量为进货数量,费用为 \(a_i\) 。
- \((i, j) \to h_i\) ,流量为 \(+ \infty\) ,费用为 \(0\) 。
- \((i, j) \to (i, j - 1)\) ,流量为 \(+ \infty\) ,费用为 \(0 \) 。
- \(h_i \to T\) ,流量为 \(m\) ,费用为 \(0\) 。
- 在 \(i\) 第一次进货时,将 \(S \to (i, j)\) 的流量分一个单位出来,费用为 \(a_i + s_i\) 。
由于有时序的限制,考虑增量费用流模型,按时间升序加入出售点 \(h_i\) :
- 增广路:\(S \to (k, j) \to \cdots \to (i, j) \to h_i \to T\) ,根据 \(k\) 与 \(i\) 的大小关系可以分为三类。
- 环:由于只有 \(S\) 出边有权,因此只要考虑 \(S\) 所在的环 \(S \to (i_1, j_1) \to h_{i_1} \to T \to h_{i_2} \to (i_2, j_2) \to S\) ,这显然不如增广路,因为 \(T \to S\) 的费用 \(\leq 0\) 。
因此可以得到 \(S\) 的出边不会退流,因此前一天的出售方案是当前出售方案的子集。
考虑先求出前 \(p\) 天的最优方案,推到前 \(p - 1\) 天的最优方案,只要保留最贵的 \((p - 1) \times m\) 个,因此只要排序一遍即可。问题转化为求前 \(p\) 天的最优方案。此时无需考虑时序的限制,可以按照时间倒序加入菜和出售点,则增广路只有两种:卖掉今天的菜、卖掉之间加入的菜。
用堆维护这个过程,时间复杂度 \(O(m \times \max \{ p_i \} \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e5 + 7;
struct Vegetable {
int a, s, c, x;
} p[N];
vector<int> ins[N];
int qry[N], sold[N];
int n, m, k;
signed main() {
scanf("%d%d%d", &n, &m, &k);
for (int i = 1; i <= n; ++i)
scanf("%d%d%d%d", &p[i].a, &p[i].s, &p[i].c, &p[i].x);
for (int i = 1; i <= k; ++i)
scanf("%d", qry + i);
int tim = *max_element(qry + 1, qry + k + 1);
for (int i = 1; i <= n; ++i) {
if (p[i].x)
ins[min((p[i].c + p[i].x - 1) / p[i].x, tim)].emplace_back(i);
else
ins[tim].emplace_back(i);
}
priority_queue<pair<int, int> > q;
vector<int> surplus;
for (int i = tim; i; --i) {
for (int it : surplus)
q.emplace(p[it].a + (sold[it] ? 0 : p[it].s), it);
surplus.clear();
for (int it : ins[i])
q.emplace(p[it].a + (sold[it] ? 0 : p[it].s), it);
int cnt = m;
while (!q.empty() && cnt) {
int x = q.top().second;
if (sold[x]) {
int now = min(p[x].c - 1ll * p[x].x * (i - 1) - sold[x], (ll)cnt);
sold[x] += now, cnt -= now;
if (p[x].c - 1ll * p[x].x * (i - 1) == sold[x]) {
q.pop();
if (p[x].x)
surplus.emplace_back(x);
}
} else {
sold[x] = 1, --cnt, q.pop();
if (p[x].c - 1ll * p[x].x * (i - 1) > 1)
q.emplace(p[x].a, x);
else if (p[x].x)
surplus.emplace_back(x);
}
}
}
vector<ll> ans;
for (int i = 1; i <= n; ++i)
if (sold[i])
ans.emplace_back(p[i].a + p[i].s), ans.insert(ans.end(), sold[i] - 1, p[i].a);
sort(ans.begin(), ans.end(), greater<ll>());
ans.insert(ans.begin(), 0);
for (int i = 1; i < ans.size(); ++i)
ans[i] += ans[i - 1];
for (int i = 1; i <= k; ++i)
printf("%lld\n", ans[min((int)ans.size() - 1, qry[i] * m)]);
return 0;
}
任意流消圈模型
[AGC018C] Coins
有 \(x + y + z\) 个人,第 \(i\) 个人有 \(a_i\) 个金币、\(b_i\) 个银币、\(c_i\) 个铜币。
要选出 \(x\) 个人获得其金币、 \(y\) 个人获得其银币、 \(z\) 个人获得其铜币。
在不重复选人的情况下,最大化获得的币的总数。
\(x + y + z \leq 10^5\)
建立费用流模型:
- \(S\) 向每个人连流量为 \(1\) 、费用为 \(0\) 的边。
- 每个人向三个虚点分别连流量为 \(1\) 、费用分别为 \(a_i, b_i, c_i\) 的边。
- 三个虚点向 \(T\) 分别连流量为 \(x, y, z\) 、费用为 \(0\) 的边。
最大费用流即为答案。
由于有 \(x, y, z\) 三个限制,因此考虑先求出任意流然后消圈。可以发现环一定建立在每个人和虚点之间,考虑其实际意义就是将一些人的选择互换,具体情况可以分为基本的五种:
- \(A \to B, B \to C, C \to A\) 。
- \(A \to C, C \to B, B \to A\) 。
- \(A \to B, B \to A\) 。
- \(A \to C, C \to A\) 。
- \(B \to C, C \to B\) 。
开六个堆维护每一步转换的决策即可,时间复杂度 \(O(n \log n)\) 。
类似的题目:CF730I Olympiad in Programming and Sports
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const ll inf = 1e18;
const int N = 1e5 + 7;
int id[N];
struct Heap {
priority_queue<pair<int, int> > q;
int op;
inline Heap(int _op) : op(_op) {}
inline void maintain() {
while (!q.empty() && id[q.top().second] != op)
q.pop();
}
inline bool empty() {
return maintain(), q.empty();
}
inline pair<int, int> top() {
return maintain(), q.top();
}
inline void emplace(int x, int id) {
q.emplace(x, id);
}
} qab(1), qac(1), qbc(2), qba(2), qca(3), qcb(3);
int a[N], b[N], c[N];
int x, y, z;
inline void updateA(int x) {
id[x] = 1, qab.emplace(b[x] - a[x], x), qac.emplace(c[x] - a[x], x);
}
inline void updateyB(int x) {
id[x] = 2, qba.emplace(a[x] - b[x], x), qbc.emplace(c[x] - b[x], x);
}
inline void updateC(int x) {
id[x] = 3, qca.emplace(a[x] - c[x], x), qcb.emplace(b[x] - c[x], x);
}
signed main() {
scanf("%d%d%d", &x, &y, &z);
for (int i = 1; i <= x + y + z; ++i)
scanf("%d%d%d", a + i, b + i, c + i);
ll ans = 0;
for (int i = 1; i <= x; ++i)
ans += a[i], updateA(i);
for (int i = x + 1; i <= x + y; ++i)
ans += b[i], updateyB(i);
for (int i = x + y + 1; i <= x + y + z; ++i)
ans += c[i], updateC(i);
for (;;) {
ll res = -inf, res1 = -inf, res2 = -inf, res3 = -inf, res4 = -inf, res5 = -inf;
if (!qab.empty() && !qba.empty())
res = max(res, res1 = qab.top().first + qba.top().first);
if (!qbc.empty() && !qcb.empty())
res = max(res, res2 = qbc.top().first + qcb.top().first);
if (!qac.empty() && !qca.empty())
res = max(res, res3 = qac.top().first + qca.top().first);
if (!qab.empty() && !qbc.empty() && !qca.empty())
res = max(res, res4 = qab.top().first + qbc.top().first + qca.top().first);
if (!qac.empty() && !qcb.empty() && !qba.empty())
res = max(res, res5 = qac.top().first + qcb.top().first + qba.top().first);
if (res <= 0)
break;
ans += res;
if (res1 == res) {
int x = qab.top().second, y = qba.top().second;
updateyB(x), updateA(y);
} else if (res2 == res) {
int y = qbc.top().second, z = qcb.top().second;
updateC(y), updateyB(z);
} else if (res3 == res) {
int x = qac.top().second, z = qca.top().second;
updateC(x), updateA(z);
} else if (res4 == res) {
int x = qab.top().second, y = qbc.top().second, z = qca.top().second;
updateyB(x), updateC(y), updateA(z);
} else {
int x = qac.top().second, z = qcb.top().second, y = qba.top().second;
updateC(x), updateyB(z), updateA(y);
}
}
printf("%lld", ans);
return 0;
}
[ARC168F] Up-Down Queries
对于长度为 \(n\) 的操作序列 \(x_{1 \sim n}\) ,定义 \(f(x_{1 \sim n})\) 为:有一个长度为 \(m\) 的全 \(0\) 序列 \(y_{1 \sim m}\) ,对 \(i = 1, 2, \cdots, n\) 一次进行如下操作:
- 对于 \(1 \leq j \leq x_i\) ,令 \(y_j \gets \max(y_j - 1, 0)\) 。
- 对于 \(x_i < j \leq m\) ,令 \(y_j \gets y_j + 1\) 。
所有操作进行后,定义 \(f(x_{1 \sim n}) = \sum_{i = 1}^m y_i\) 。
给定初始操作序列,\(q\) 次对操作序列的单点修改操作,每次修改后求 \(f(x_{1 \sim n})\) 。
\(n \leq 2.5 \times 10^5\)
先将一次操作为将 \(y_{x_i + 1 \sim m}\) 加上 \(2\) ,然后将全局非 \(0\) 的位置减去 \(1\) 。
不难发现 \(y_{1 \sim m}\) 始终单调不降,考虑记 \(c_i = y_i - y_{i - 1}\) (钦定 \(y_0 = 0\) ),则 \(f(x_{1 \sim n}) = \sum_{i = 1}^m c_i (m - i + 1)\) 。考虑将其视为每次往可重集 \(S\) 中放 \(c_i\) 个 \(m - i + 1\) ,然后对所有数求和。考虑一次操作的影响:
- 将 \(y_{x_i + 1 \sim m}\) 加上 \(2\) :等价于将 \(c_{x_i + 1}\) 加上 \(2\) ,即向 \(S\) 中放两个 \(m - x_i\) 。
- 全局非 \(0\) 的位置减去 \(1\) :将第一个非 \(0\) 的 \(c\) 减去 \(1\) ,即删去集合中的最大值。
问题转化为:维护一个初始为空的可重集,每次加入两个 \(m - x_i\) ,然后删去集合最大值。考虑将删除最大值变成删除任意数,最小化删去的数的和。
建立费用流模型:
- \((S, i, 2, m - x_i)\) :加入两个 \(m - x_i\) 。
- \((i, T, 1, 0)\) :删去一个数。
- \((i, i + 1, +\infty, 0)\) :当前数可以之后删去。
最小费用最大流即为答案。
考虑任意流消圈模型,先找到一个初始最大流:\(S \to i \to T\) ,然后不断找负环增广。
由于只有 \(S\) 所连的边有费用,因此只有两种负环:
- \(S \to i \to i + 1 \to \cdots \to j - 1 \to j \to S\) :限制为存在边 \(S \to i\) 和边 \(j \to S\) 。
- \(S \to j \to j - 1 \to \cdots \to i + 1 \to i \to S\) :限制为存在边 \(S \to j\) 和边 \(i \to S\) ,且 \(j \to i\) 路径上均存在反向边。
开两棵线段树,一棵维护与 \(S\) 连的边的情况,一棵维护链上的反向边。由于是单调修改,因此每次找到的负环一定包含修改的位置,且每次只会增广 \(O(1)\) 次,时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
const int N = 2.5e5 + 7;
int a[N], flow[N];
ll ans;
int n, m, q;
namespace SMT {
pair<ll, int> mn[N << 2], mx[N << 2];
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
void build(int x, int l, int r) {
mn[x] = make_pair(0, l), mx[x] = make_pair(0, r);
if (l == r) {
flow[l] = 1;
return;
}
int mid = (l + r) >> 1;
build(ls(x), l, mid), build(rs(x), mid + 1, r);
}
void update(int x, int nl, int nr, int p, int k) {
if (nl == nr) {
flow[nl] += k;
mx[x] = (flow[nl] < 2 ? make_pair(a[nl], nl) : make_pair(-inf, 0));
mn[x] = (flow[nl] ? make_pair(a[nl], nl) : make_pair(inf, 0));
return;
}
int mid = (nl + nr) >> 1;
if (p <= mid)
update(ls(x), nl, mid, p, k);
else
update(rs(x), mid + 1, nr, p, k);
mn[x] = min(mn[ls(x)], mn[rs(x)]), mx[x] = max(mx[ls(x)], mx[rs(x)]);
}
pair<int, int> querymin(int x, int nl, int nr, int l, int r) {
if (l <= nl && nr <= r)
return mn[x];
int mid = (nl + nr) >> 1;
if (r <= mid)
return querymin(ls(x), nl, mid, l, r);
else if (l > mid)
return querymin(rs(x), mid + 1, nr, l, r);
else
return min(querymin(ls(x), nl, mid, l, r), querymin(rs(x), mid + 1, nr, l, r));
}
pair<int, int> querymax(int x, int nl, int nr, int l, int r) {
if (l <= nl && nr <= r)
return mx[x];
int mid = (nl + nr) >> 1;
if (r <= mid)
return querymax(ls(x), nl, mid, l, r);
else if (l > mid)
return querymax(rs(x), mid + 1, nr, l, r);
else
return max(querymax(ls(x), nl, mid, l, r), querymax(rs(x), mid + 1, nr, l, r));
}
} // namespace SMT
inline pair<int, int> querymin(int l, int r) {
return l <= r ? SMT::querymin(1, 1, n, l, r) : make_pair(inf, 0);
}
inline pair<int, int> querymax(int l, int r) {
return l <= r ? SMT::querymax(1, 1, n, l, r) : make_pair(-inf, 0);
}
namespace SGT {
int mn[N << 2], tag[N << 2];
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
inline void spread(int x, int k) {
mn[x] += k, tag[x] += k;
}
inline void pushdown(int x) {
if (tag[x])
spread(ls(x), tag[x]), spread(rs(x), tag[x]), tag[x] = 0;
}
void update(int x, int nl, int nr, int l, int r, int k) {
if (l <= nl && nr <= r) {
spread(x, k);
return;
}
pushdown(x);
int mid = (nl + nr) >> 1;
if (l <= mid)
update(ls(x), nl, mid, l, r, k);
if (r > mid)
update(rs(x), mid + 1, nr, l, r, k);
mn[x] = min(mn[ls(x)], mn[rs(x)]);
}
int searchl(int x, int nl, int nr, int l, int r) {
if (r < nl || l > nr || mn[x])
return 0;
if (nl == nr)
return nl;
pushdown(x);
int mid = (nl + nr) >> 1, res = searchl(ls(x), nl, mid, l, r);
return res ? res : searchl(rs(x), mid + 1, nr, l, r);
}
int searchr(int x, int nl, int nr, int l, int r) {
if (r < nl || l > nr || mn[x])
return 0;
if (nl == nr)
return nl;
pushdown(x);
int mid = (nl + nr) >> 1, res = searchr(rs(x), mid + 1, nr, l, r);
return res ? res : searchr(ls(x), nl, mid, l, r);
}
} // namespace SGT
inline void update1(int x) {
if (flow[x] == 2)
return;
int p = (x > 1 ? SGT::searchr(1, 1, n, 1, x - 1) : 0);
auto res = min(querymin(x + 1, n), querymin(p + 1, x - 1));
if (res.first >= a[x])
return;
SMT::update(1, 1, n, x, 1), SMT::update(1, 1, n, res.second, -1);
ans -= a[x] - a[res.second];
if (res.second < x)
SGT::update(1, 1, n, res.second, x - 1, -1);
else
SGT::update(1, 1, n, x, res.second - 1, 1);
}
inline void update2(int x) {
if (!flow[x])
return;
int p = SGT::searchl(1, 1, n, x, n);
auto res = max(querymax(1, x - 1), querymax(x + 1, p));
if (res.first <= a[x])
return;
SMT::update(1, 1, n, x, -1), SMT::update(1, 1, n, res.second, 1);
ans -= a[res.second] - a[x];
if (res.second < x)
SGT::update(1, 1, n, res.second, x - 1, 1);
else
SGT::update(1, 1, n, x, res.second - 1, -1);
}
signed main() {
scanf("%d%d%d", &n, &m, &q);
SMT::build(1, 1, n);
for (int i = 1; i <= n; ++i) {
scanf("%d", a + i), a[i] = m - a[i];
SMT::update(1, 1, n, i, 0), ans += a[i] * (2 - flow[i]);
update1(i), update1(i);
}
while (q--) {
int x, k;
scanf("%d%d", &x, &k);
k = m - k, swap(a[x], k);
SMT::update(1, 1, n, x, 0), ans += (a[x] - k) * (2 - flow[x]);
if (a[x] > k)
update1(x), update1(x);
else
update2(x), update2(x);
printf("%lld\n", ans);
}
return 0;
}

浙公网安备 33010602011771号