CF1618G - Trader Problem
第一次看这道题目时,很容想到分组,将两个人的货物标记好是 a a a的还是 b b b的,然后按照价值从小到大进行排序,从左到右遍历一下,间隔不超过 k k k的段分成一组。然后看每一组里有多少个货物是 a a a的,就从那一组里选取多少个最大价值的货物,将那些价值加入到答案中。因为,每一个组的货物之间一定可以呼唤,即一定存在一种方法,让一个货物换到 a a a的手中。将每一个 k k k分别按照这样的方法进行计算。
但是这样会超时,因为时间复杂度是 O ( n q ) O(nq) O(nq)的,是不能接受的时间复杂度(cf上可以过67个test,一度以为过了)。
我们发现,间隔超过一定值的 k k k,当 k k k大到一定程度的时候,分组与分组之间就可以合并到一起了,因为合并之后,每一个货物之间的差值不会超过 k k k。而这样的合并操作,随着 k k k的变大,并不会逆转。所以离线处理,将 q q q个询问的 k k k从小到大排列起来。
现在处理 n + m n+m n+m个货物。将这 n + m n+m n+m个货物标记好是 a a a的还是 b b b的,然后按照货物价值从小到大进行排序。开两个multi_set(因为会有重复元素): A i , B i A_i,B_i Ai,Bi,分别表示第 i i i组中 a a a的货物的集合和 b b b的货物的集合。在没有进行合并的时候,一共有 n + m n+m n+m组,每一组中 A i A_i Ai和 B i B_i Bi这两个集合中必有一个是空集合,一个是包含一个货物的集合。
现在要给查询操作和合并区间的操作进行一个顺序安排。
我们查询操作的顺序与 k k k的大小有关,具体的顺序是遵循 k k k从小到大的顺序,因为这样的顺序,各个组的流动情况是趋于不断合并的,且这个过程是不会逆转的,方便实现。
什么时候进行合并,这取决于组与组之间的差值。如果当前的 k k k已经等于这个差值,那么这两组就可以合并。而初始状态的差值一共只有 n + m − 1 n+m-1 n+m−1个,所以进行合并的判断操作只有 n + m − 1 n+m-1 n+m−1次。而合并操作之间的顺序与差值 d d d有关, d d d越小,操作则越优先。
当我们开始查询 k k k时,如果存在组与组之间的差等于 k k k(小于 k k k的我们假设已经都合并完了),那么我们需要先将组合进行合并,后进行查询操作。通过这个排序方法,我们得到了排好序的 n + m + q − 1 n+m+q-1 n+m+q−1个操作,包括 n + m − 1 n+m-1 n+m−1个合并操作和 q q q个查询操作。
现在考虑合并操作的实现:
假设现在要将 x x x集合与 y y y集合合并,我们就暴力地将 y y y合并的所有元素插入到 x x x集合中(这里的集合包括了每个组的 A A A集合和 B B B集合)。
但是暴力插入会有非常多的时间消耗,我们考虑启发式合并。启发式合并的思想就是每次合并时,只进行两个集合中较小者元素数量的次数的操作。换句话说,就是把元素少的集合的所有元素暴力插入到元素多的集合当中去。
这样为什么可以提高效率,我们尝试感性理解一下:
考虑每一个元素被插入的次数,如果被插入,那么意味着这个元素所在的集合是元素个数较少的,插入之后,这个元素所在的集合的元素个数一定相比以前是至少翻倍的,即每次进行一次合并,这个元素所在的集合的长度都会至少翻倍。那么这个元素最多会被插入多少次?就是 log n \log n logn次(这里 n n n是所有的元素个数)。每一个元素最多被插入 log n \log n logn次,那么整体插入的时间复杂度就是 O ( n log n ) O(n\log n) O(nlogn)。
暴力插入后,我们要维护答案。这一次集合的合并为我们带来的贡献,就是原集合中最后被 a a a选中的货物和现在的集合中被 a a a选中的货物之间的价值和的差别。产生答案贡献的行为,本质上,是新形成的 a a a的物品中价值最少的低于 b b b的物品中价值最大的的情况下,这两种物品进行的交换。设 a a a的物品中价值最少的货物的价值为 w a w_a wa, b b b的物品中价值最大的货物的价值是 w b w_b wb,那么这一次行动,为答案带来的贡献就是 w b − w a w_b-w_a wb−wa。计算完贡献后将这个贡献加进答案 s u m sum sum当中( s u m sum sum具体维护方法见后文),然后暴力移动这两个货物,将这两个货物在一个组内的两个小集合 A A A和 B B B中交换位置。这样的行动一直进行到当前的情况不满足 w a < w b w_a < w_b wa<wb,遇到这种情况时停止。
最后,按照并查集的方式,将 y y y的父亲标识成 x x x。这样每次进行小组的组合时,先找到双方最上级的集合,然后比较两个集合的大小哪一个更大,将更大的设为 x x x,更小的设为 y y y,然后执行上述的组合的步骤即可。
答案查询操作怎么实现?
我们全局维护一个 s u m sum sum,表示当前的操作做完时,现在 a a a最多能获得价值和多少的货物。改变这个值的只有合并操作,所以遇到一个询问操作,直接将这个当前的 s u m sum sum作为这一次询问的答案即可。所有操作进行之前, s u m sum sum的初始值,是 a a a初始的 n n n个货物的价值和。
最后把所有操作按照编号顺序排序,只输出询问操作的答案。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
multiset<LL> sa[400005], sb[400005];
LL sum = 0;
int n, m, Q;
struct QUERY {
LL k, t, id, ans;
};
struct PAIR {
LL x, y;
bool operator < (const PAIR &A) const {
return (x == A.x) ? (y < A.y) : (x < A.x);
}
};
int a[200005], b[200005], fa[400005];
int find(int x) {
return (x == fa[x]) ? x : (fa[x] = find(fa[x]));
}
void merge(int x, int y) {
x = find(x); y = find(y);
if (sa[x].size() + sb[x].size() < sa[y].size() + sb[y].size()) {
swap(x, y);
}
for (int tmp: sa[y]) sa[x].insert(tmp);
for (int tmp: sb[y]) sb[x].insert(tmp);
while (sa[x].size() and sb[x].size() and *sa[x].begin() < *sb[x].rbegin()) {
int mi = *sa[x].begin(), mx = *sb[x].rbegin();
sum -= mi; sum += mx;
sa[x].erase(sa[x].find(mi));
sb[x].erase(sb[x].find(mx));
sa[x].insert(mx);
sb[x].insert(mi);
}
fa[y] = x;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin >> n >> m >> Q;
sum = 0;
for (int i = 0; i <= n + m; ++i) {
fa[i] = i;
}
vector<PAIR> v;
v.push_back({0, 0});
for (int i = 1; i <= n; ++i) {
cin >> a[i];
v.push_back({a[i], 1});
sum += a[i];
}
for (int i = 1; i <= m; ++i) {
cin >> b[i];
v.push_back({b[i], 0});
}
sort(v.begin(), v.end());
vector<QUERY> q;
for (int i = 1; i <= n + m; ++i) {
if (v[i].y == 1) sa[i].insert(v[i].x);
else sb[i].insert(v[i].x);
}
for (int i = 1; i < n + m; ++i) {
q.push_back({v[i + 1].x - v[i].x, 0, i, 0});
}
for (int i = 1; i <= Q; ++i) {
int tmp; cin >> tmp;
q.push_back({tmp, 1, i, 0});
}
sort(q.begin(), q.end(), [](const QUERY &A, const QUERY &B) {
return (A.k == B.k) ? (A.t < B.t) : (A.k < B.k);
});
for (auto &qq: q) {
if (qq.t) qq.ans = sum;
else merge(qq.id, qq.id + 1);
}
sort(q.begin(), q.end(), [](const QUERY &A, const QUERY &B) {
return A.id < B.id;
});
for (auto qq: q) {
if (qq.t) cout << qq.ans << '\n';
}
return 0;
}