【补题笔记】启发式合并-CF1618G Trader Problem

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+m1个,所以进行合并的判断操作只有 n + m − 1 n+m-1 n+m1次。而合并操作之间的顺序与差值 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+q1个操作,包括 n + m − 1 n+m-1 n+m1个合并操作和 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 wbwa。计算完贡献后将这个贡献加进答案 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;
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值