启发式合并详解 (基于CF 1620E - Replace the Numbers)

前言:
这是Educational Codeforces Round 119 (Rated for Div. 2)的第五题,
这个题我最开始使用从后往前的方法写的,复杂度 O ( n ) O(n) O(n)
交完以后我看看了题解,题解提供了两种办法,第二种是用启发式合并(英文名叫 small to large method),看到这我别提有多激动了,因为自从学了启发式合并以后一直没有找到适合练手的题目。
那我就通过这个题来好好梳理一下启发式合并吧。

例题题面
You have an array of integers (initially empty).

You have to perform q queries. Each query is of one of two types:

“1 x” — add the element x to the end of the array;
“2 x y” — replace all occurrences of x in the array with y.
Find the resulting array after performing all the queries.

Input
The first line contains a single integer q (1≤q≤5⋅105) — the number of queries.

Next q lines contain queries (one per line). Each query is of one of two types:

“1 x” (1≤x≤5⋅105);
“2 x y” (1≤x,y≤5⋅105).
It’s guaranteed that there is at least one query of the first type.

Output
In a single line, print k integers — the resulting array after performing all the queries, where k is the number of queries of the first type.

啥是启发式合并
启发式合并,就是高效的、从小到大的合并。它给人的直接感觉很弱,不像线段树什么的能让你瞬间体会到算法的精妙性。实际上,启发式合并可以将很多 n 2 n^2 n2的复杂度降级为 n l o g n nlogn nlogn,如果敏锐地洞察出一个题可以启发式合并,我们的算法性能将得到极大的提升。

启发式合并怎么做
以此题为例,我们为每个数开一个线性表(最好用vector)。执行操作 1 1 1时,若x是第n个追加的元素,则将n放入第 x x x个线性表(也就是说一个线性表里面装了这个值出现的所有位置)。执行操作 2 2 2时,我们要将x出现的位置改为 y y y,也就是要把第 x x x个线性表全部倒入第 y y y个线性表。如果暴力来处理的话,必定会超时。那么怎么优化呢?这里无非是对两个线性表的一个合并,我们利用启发式合并的策略,将较小的那个线性表倒入较大的线性表即可(此处的大小指元素个数)。合并完之后,如果表名不一致颠到,交换一下即可(vector的交换机制是改指针,所以复杂度是 O ( 1 ) O(1) O1的)。需要注意的是,当 x = y x=y x=y时,应当直接跳过。

启发式合并的复杂度
本算法的主要时间消耗在于合并两个线性表(或者说是集合)。
每次合并操作,我们都将小集合倒入大集合,只有小集合中的元素发生了移动,而且最终集合的大小至少是小集合的两倍。
假设 a a a被移动了 b b b次,也就是说 a a a所在的集合的大小发生了b次倍增(有可能比倍增还大)。所以 n > = 2 b n>=2^b n>=2b,也就是说 b < = l o g n b<=logn b<=logn
因此所有元素移动的总次数是 n l o g n nlogn nlogn
仔细阅读我们发现,在推导这个复杂度的时候,我们分析的是移动次数最多的那个元素的最多移动次数,所以这个算法的效率往往比我们预期的还要快很多。

例题代码

#include<bits/stdc++.h>
#define endl '\n'
using namespace std;

vector<int>v[500010];
int a[500010];
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);cout.tie(nullptr);
	
	int q,num=0;cin>>q;
	while(q--){
		int op,x,y;
		cin>>op;
		if(op==1){
			cin>>x;
			v[x].push_back(++num);
		}
		else{
			cin>>x>>y;
			if(x==y) continue;
			if(v[x].size()>v[y].size()){
				for(auto i:v[y]){
					v[x].push_back(i);
				}
				v[y].clear();
				swap(v[x],v[y]);
			}
			else{
				for(auto i:v[x]){
					v[y].push_back(i); 
				}
				v[x].clear();
			}
		}
	} 
	for(int i=1;i<=500000;i++){
		for(auto j:v[i]){
			a[j]=i;
		}
	}
	for(int i=1;i<=num;i++){
		cout<<a[i]<<' ';
	}cout<<endl;
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

组合,我有特殊的计数技巧

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值