先介绍一下并查集的基本功能
基础的并查集主要有两种.
第一种是普通的并查集,主要用来处理无权的相对关系,比如说A和B是一类人,B和C是一类人,那么A和C也是一类人.
第二种是带权的并查集.种类并查集也是其中的一种.具体下面会介绍.
1.无权并查集
先引入一个问题.会给N个关系.A B代表A和B是一类东西.且具有传递性.
然后给Q个询问.问A B 是否是同一类东西.如果要O(1)的询问,我们可以建立一个数组.下标代表数值,数组的值代表种类. 这种方式询问确实是O(1)的,但修改需要O(N)的.不够优秀.我们需要引入一种新的数据结构来解决这个问题.
和上面的解法类似.不过我们引入一个新的概念.让每个数值都有一个代表元.数组名字记为fa.
一开始全部数值的代表元都是它自己.之后如果A B 是同类人,就让fa[A]=fa[B]这里的fa不是指数组,而是指他的真正代表元.因为在多次合并之后fa[A]不一定是A的代表元了.可能是fa[fa[A]].这是一个套娃的问题.所以.递归求解.
如果看文字看不懂,就画图理解,无论是数据结构还是算法.画图都是一个很直观的理解方法.
在这里.顶部的就是下面几个节点的代表元.要找到真正的代表元.可以用一个简单的get函数来表示.return x == fa[x]?x:get(fa[x]); 这个写法不够优秀.凡是树的问题就要考虑退化问题.如果退化成为一条链的时候.查找的效率还是O(n)的.所以要压缩路径.我们可以在一次递归后直接把fa[x]指向它的跟节点.
非递归版本可以更好的理解它.(效率也更高).
int get(int x){
int k,j,r;
r = x;
while(r != fa[r]) r = fa[r];
k = x;
while(k != r){
j = fa[k];
fa[k] = r;
k = j;
}
return r;
}
A B合并就很简单了.直接上代码了.(其实是有按秩优化的方法,但是其实影响不是很大.压缩路径之后效率均摊下来是够用的)
int unite(int x,int y){
x = get(x), y = get(y);
fa[x] = y;
}
一道很基础的例题(其实不是特别特别基础.建议配合离散化食用.)
补充一下离散化的知识点吧.所谓离散化,就是忽略具体数值,只记录相对的大小.比如说100 200 300 400 500 离散化之后可以是1 2 3 4 5.这个不会改变他本身在数组中的位置,但是可以缩小它本身的值.可以理解为给原先数组中的每一个元素都取了一个新名字,这个新名字就是离散化数组里面该元素对应的下标.
前面提到了并查集fa数组的下标是它的数值.在这题里数值是有1≤i,j≤1000000000的.太大了.数组开不下
但是1≤n≤1000000也就是说就算所有的i j都不同.最多也才2000000个数字.离散化之后下标就是0-1999999.这样子数组就开的下了.而离散化也是有模板的.
我一般是用vector存原先的数值,然后排序去重拿到处理好的数组后用lower_bound来得到新数组里面的值.
ps:这几个函数不熟悉的话建议百度一下.
离散化模板代码:
void LS(vector<int> &x){//x里面储存着原先所有的信息
sort(x.begin(),x.end());
x.erase(unique(x.begin(),x.end()),x.end());
}
int getnewpos(vector<int> x,int xx){
return lower_bound(x.begin(),x.end(),xx)-x.begin();
}
本题代码
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
const int mod = 9901;
LL gcd(LL a,LL b){return b == 0 ? a : gcd(b,a%b);}
int fa[N];
vector<int> all;
int get(int x){
int k,j,r;
r = x;
while(r != fa[r]) r = fa[r];
k = x;
while(k != r){
j = fa[k];
fa[k] = r;
k = j;
}
return r;
}
int getpos(int x){
return lower_bound(ALL(all),x)-all.begin();
}
int main(){
int t;
cin >> t;
while(t--){
int n;
cin >> n;
all.clear();
vector<pii> v1,v2;
fir(i,1,n){
int x,y,q;
cin >> x >> y >> q;
all.pb(x);
all.pb(y);
if(q) v1.pb(make_pair(x,y));
else v2.pb(make_pair(x,y));
}
sort(ALL(all));
all.erase(unique(ALL(all)),all.end());
fir(i,0,(int)all.size()-1){
fa[i] = i;
}
fir(i,0,(int)v1.size()-1){
int x = getpos(v1[i].ft), y = getpos(v1[i].sd);
int fx = get(x), fy = get(y);
fa[fx] = fy;
}
bool f = 1;
fir(i,0,(int)v2.size()-1){
int x = getpos(v2[i].ft), y = getpos(v2[i].sd);
int fx = get(x), fy = get(y);
if(fx == fy){
f = 0;
break;
}
}
if(f) puts("YES");
else puts("NO");
}
return 0;
}
2.加深对并查集的理解
引入一道例题来彻底搞懂并查集.
银河英雄传说
先考虑怎么算A B相隔了多少个飞艇.先算出A和根节点的距离d1,再算出B和跟节点的距离d2.那么abs(d1-d2)-1就是相隔的数量.我们可以用一个d[i]数组来维护这个信息.同时也要更改压缩路径的写法,只要先求出i到跟节点的d 然后每次减掉d[i]就可以了
代码
int get(int x){
int k,j,r;
r = x;
int tmp = 0;
while(r != fa[r]){
tmp += d[r];
r = fa[r];
}
k = x;
while(k != r){
j = fa[k];
fa[k] = r;
int cur = d[k];
d[k] = tmp;
tmp -= cur;
k = j;
}
return r;
}
那么合并呢?因为是把i接到j的后面.把i接到j后面之后等于i上每个节点离跟节点都增加了size[j]距离,也就是j这棵数的大小.
void unite(int x,int y){
int fx = get(fx), fy = get(fy);
fa[fx] = fy;
d[fx] = s[fy];
s[fy] += s[fx];
}
AC代码
#pragma GCC optimize(3)
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#include <bits/stdc++.h>
using namespace std;
const int N = 3e4+10;
const int mod = 9901;
LL gcd(LL a,LL b){return b == 0 ? a : gcd(b,a%b);}
int fa[N],d[N],s[N];
int get(int x){
int k,j,r;
r = x;
int tmp = 0;
while(r != fa[r]){
tmp += d[r];
r = fa[r];
}
k = x;
while(k != r){
j = fa[k];
fa[k] = r;
int cur = d[k];
d[k] = tmp;
tmp -= cur;
k = j;
}
return r;
}
pii a[N];
int main(){
ios::sync_with_stdio(false);
int t,x,y;
cin >> t;
fir(i,1,N-1) fa[i] = i,son[i] = i,s[i] = 1;
fir(i,1,t){
char op;
cin >> op >> x >> y;
int fx = get(x), fy = get(y);
if(op == 'M'){
fa[fx] = fy;
d[fx] = s[fy];
s[fy] += s[fx];
}
else{
if(fx == fy) cout << abs(d[y]-d[x])-1 << endl;
else cout << -1 << endl;
}
}
return 0;
}
3.带权并查集
先确保能理解上一题的解法之后再来学习带权并查集.
同样要解决两个问题,get和unite,get的操作和上一题其实是一样的,这里的关键是unite操作.
用一个图来理解一下
观察上面的图.假设我们合并后让fa[fx] = fy.那么d[fx]=?
上面两条从x到fy的路径的权值是相同的.即v+d[y] = d[x]+d[fx].
所以d[fx] = v+d[y]-d[x]. 这个就是合并的关键部分.
转化成代码就是
void unite(int x,int y,int v){
int fx = get(x), fy = get(y);
fa[fx] = fy;
d[fx] = v + f[y] - f[x];
}
有了这两个操作之后.来解决一些例题.
奇偶游戏
性质:[l,r]的和为奇数说明sum[r] 和 sum[l-1]的奇偶性不相同.为偶说明相同.
这个性质点明了题目的本质:给一个A B.会告诉A B是相同还是不相同,然后看有没有矛盾点.
这个就是带权并查集的一个分支:种类并查集.d[i]表示i和跟节点是否相同,0表示相同,1表示不相同.初始化全为0. 然后get和unite的操作从±法变成了异或.大家可以画几个图体会一下1和0的关系传递是如何传递的.不一定要用异或.用一个自己能理解,能写对的方法就可以了.
不过和上面第一个例题一样.需要先离散化.
代码
#pragma GCC optimize(3)
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4+10;
const int mod = 9901;
LL gcd(LL a,LL b){return b == 0 ? a : gcd(b,a%b);}
int fa[N],d[N];
int get(int x){
int k,j,r;
r = x;
int tmp = 0;
while(r != fa[r]){
tmp ^= d[r];
r = fa[r];
}
k = x;
while(k != r){
j = fa[k];
fa[k] = r;
int cur = d[k];
d[k] = tmp;
tmp ^= cur;
k = j;
}
return r;
}
vi all;
int getpos(int x){ return lower_bound(ALL(all),x)-all.begin();}
pair<pair<int,int>,bool> p[N];
int main(){
ios::sync_with_stdio(false);
int n,q;
cin >> n >> q;
fir(i,0,N-1) fa[i] = i;
fir(i,1,q){
int x,y;
string op;
cin >> x >> y >> op;
if(op[0] == 'e'){
p[i].ft = {x-1,y};
p[i].sd = true;
}
else{
p[i].ft = {x-1,y};
p[i].sd = false;
}
all.pb(x-1);
all.pb(y);
}
sort(ALL(all));
all.erase(unique(ALL(all)),all.end());
int ans = q;
fir(i,1,q){
int l = getpos(p[i].ft.ft), r = getpos(p[i].ft.sd);
int fx = get(l), fy = get(r);
if(fx != fy){
fa[fx] = fy;
if(p[i].sd)
d[fx] = d[l]^d[r]^0;
else
d[fx] = d[l]^d[r]^1;
continue;
}
// cout << fx << " " << fy << endl;
if(p[i].sd){
if(d[l] != d[r]){
ans = i-1;
break;
}
}
else{
if(d[l] == d[r]){
ans = i-1;
break;
}
}
}
cout <<ans << endl;
return 0;
}
再给一道权值并查集的经典题.做法就不给了.可以自己思考.最关键的是要把握住关系之间是如何传递的.
食物链
给个参考代码
#pragma GCC optimize(3)
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#include <bits/stdc++.h>
using namespace std;
const int N = 5e4+10;
const int mod = 9901;
LL gcd(LL a,LL b){return b == 0 ? a : gcd(b,a%b);}
int fa[N],d[N];
int get(int x){
int k,j,r;
r = x;
int tmp = 0;
while(r != fa[r]){
tmp = (tmp+d[r])%3;
r = fa[r];
}
k = x;
while(k != r){
j = fa[k];
fa[k] = r;
int cur = d[k];
d[k] = tmp;
tmp = (tmp-cur+3)%3;
k = j;
}
return r;
}
int main(){
ios::sync_with_stdio(false);
int n,k;
cin >> n >> k;
fir(i,1,n) fa[i] = i,d[i] = 0;
int ans = 0;
fir(i,1,k){
int op,x,y;
cin >> op >> x >> y;
if(x > n || y > n || (op == 2 && x == y)){
ans ++ ;
continue;
}
int fx = get(x), fy = get(y);
if(fx != fy){
fa[fx] = fy;
d[fx] = (-d[x]+d[y]+op+2)%3;
continue;
}
if(op == 1){
if(d[x] != d[y]) ans++;
}
else{
if(d[x] == 1 && d[y] != 0) ans++;
else if(d[x] == 0 && d[y] != 2) ans++;
else if(d[x] == 2 && d[y] != 1) ans++;
}
}
cout << ans;
return 0;
}
4.并查集的拓展用法
前面说的都是一些很明显的并查集题目.而有些题目,它没那么的显然.而且不一定要使用并查集解决,但是并查集解决可能会更快.
超市
这题可以用堆做.复杂度O(nlogn)
也可以用并查集+贪心做.按利润从大到小排好之后.我们尽量在最后一天把这个物品卖出去.注意这里的最后一天不是字面意义上的最后一天.是最后一个能使用的天数…比如第一个物品过期天数是5,第二个也是5.那么第二个物品的最后一天就是第四天. 说到这里已经点出很关键的一步了,我们需要找到过期天数前第一个未被使用的天数.然后把这个物品卖掉. 有不少数据结构都能解决这个问题,但并查集应该是我知道的里面最优秀的一个.因为查询和合并都只需要O(1)的复杂度.
具体做法:
让fa[i]表示i前面第一个未被使用的天数.一开始fa[i] =i.如果说现在要用i这天,其实用的是fa[i]这天,用完之后fa[i]–.就ok了.
代码
#pragma GCC optimize(3)
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4+10;
const int mod = 9901;
LL gcd(LL a,LL b){return b == 0 ? a : gcd(b,a%b);}
int fa[N];
int get(int x){
int k,j,r;
r = x;
while(r != fa[r]) r = fa[r];
k = x;
while(k != r){
j = fa[k];
fa[k] = r;
k = j;
}
return r;
}
pii a[N];
int main(){
ios::sync_with_stdio(false);
int n;
while(cin >> n){
fir(i,1,n) cin >> a[i].ft >> a[i].sd;
sort(a+1,a+1+n);
fir(i,1,N-1) fa[i] = i;
LL ans = 0;
afir(i,n,1){
int fx = get(a[i].sd);
if(fx){
ans += a[i].ft*1LL;
fa[fx]--;
}
}
cout << ans << endl;
}
return 0;
}
还有一题是上学期第一次校赛时候学长出的一题.
给个题面大家思考一下做法吧.具体代码…我也没敲出来.以后再来填坑.当进阶题做吧.注意一下数据范围.线段树的做法是不可以的.3e6的范围需要O(n)的算法.
zlh最近在粉刷一面墙
墙由n块砖头组成,编号从1~n,刚全为白色(颜色0)
zlh每次可以选择一段区间[l,r](即编号从l~r)的砖头,然后将他们涂成同一种一种颜色
zlh总共操作了q次操作
问最后墙面总共有多少种颜色
Input
输入第一行两个整数n,q,表示墙面的长度与操作次数(1<=n,q<=3e6)
接下来q次操作,每次给出三个整数l,r,x(1<=l<=r<=n,0<=x<=3e6)表示给出的区间以及染色
Output
输出一行给出答案表示最后颜色的个数
今天抽空把这题给写了.这题要注意卡常.要快读而且不要用pair,不然时间会不够.
具体做法:离线一下操作.倒过来考虑.这样子的话后面的涂色是不会被前面覆盖的.等于删除了一段区间,这段区间以后再也不能被更改了.这个可以用并查集来维护.而且每个位置最多被删除一次,所以复杂度均摊下来是O(n)的.和上一题差不多,fa[i]表示i前面第一个还没被删除的位置.这题的做法还是很巧妙的.因为数据大,普通的线段树做法是没办法解决的.要离线+并查集.
加更一下:有道题目差不多但数据范围没有卡的那么死.
Mayor’s posters(比线段树解法快了4倍多吧.请忽略那一大堆线段树的代码…)
参考代码
#pragma GCC optimize(3)
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#define L 2*i
#define R 2*i+1
#include <string.h>
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstdio>
#include <queue>
using namespace std;
const int N = 2e5+10;
inline int read(){
int x = 0,f=1;char ch = getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch<='9'&&ch>='0'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
void write(int k) {
if (k < 0)
putchar('-'), k = -k;
if (k >= 10)
write(k / 10);
putchar(k % 10 + '0');
}
struct Tree{
int l,r,v;
LL sum,lz;
}tree[N*4];
int a[N],l[N],r[N],fa[N],vis[N],c[N];
void push_up(int i){
tree[i].v = max(tree[L].v,tree[R].v);
tree[i].sum = tree[L].sum + tree[R].sum;
}
void push_down(int i){
LL lz = tree[i].lz;
if(lz){
tree[i].lz = 0;
tree[L].sum = tree[L].sum + (tree[L].r-tree[L].l+1)*lz;
tree[R].sum = tree[R].sum + (tree[R].r-tree[R].l+1)*lz;
tree[L].lz = tree[L].lz + lz;
tree[R].lz = tree[R].lz + lz;
}
}
void build(int i,int l,int r){
tree[i].l = l;tree[i].r = r;
if(l >= r){
tree[i].v = tree[i].sum = a[l];
return;
}
int mid = l + r >> 1;
build(L,l,mid);build(R,mid+1,r);
push_up(i);
}
void add(int i,int p,int v){
if(tree[i].l == p && tree[i].r == p){
tree[i].v = v;
// tree[i].sum = v;
return;
}
// push_down(i);
int mid = tree[i].l + tree[i].r >> 1;
if(p <= mid) add(L,p,v);
else add(R,p,v);
push_up(i);
}
int ask(int i,int p){
if(tree[i].l == p && tree[i].r == p) return tree[i].v;
push_down(i);
int mid = tree[i].l + tree[i].r >> 1;
if(p <= mid) return ask(L,p);
else return ask(R,p);
}
void change(int i,int l,int r,LL v){
if(tree[i].l >= l && tree[i].r <= r){
tree[i].sum += (tree[i].r-tree[i].l+1)*v;
tree[i].lz += v;
return;
}
push_down(i);
int mid = tree[i].l + tree[i].r >> 1;
if(r <= mid) change(L,l,r,v);
else if(l > mid) change(R,l,r,v);
else{
change(L,l,mid,v);
change(R,mid+1,r,v);
}
push_up(i);
}
LL query(int i,int l,int r){
if(tree[i].l >= l && tree[i].r <= r) return tree[i].sum;
push_down(i);
int mid = tree[i].l + tree[i].r >> 1;
if(r <= mid) return query(L,l,r);
if(l > mid) return query(R,l,r);
return query(L,l,mid) + query(R,mid+1,r);
}
int get(int i,int l,int r){
if(tree[i].l >= l && tree[i].r <= r) return tree[i].v;
// push_down(i);
int mid = tree[i].l + tree[i].r >> 1;
if(r <= mid) return get(L,l,r);
if(l > mid) return get(R,l,r);
return max(get(L,l,mid),get(R,mid+1,r));
}
int get(int x){
int k,j,r;
r = x;
while(r != fa[r]) r = fa[r];
k = x;
while(k != r){
j = fa[k];
fa[k] = r;
k = j;
}
return r;
}
vi all;
int getpos(int x){
return lower_bound(ALL(all),x) - all.begin();
}
int main(){
int t;
scanf("%d",&t);
while(t--){
int n;
scanf("%d",&n);
all.clear();
all.pb(-1);
fir(i,1,n){
scanf("%d%d",l+i,r+i);
all.pb(l[i]);
all.pb(r[i]);
all.pb(r[i]+1);
}
sort(ALL(all));
all.erase(unique(ALL(all)),all.end());
fir(i,1,all.size()-1) fa[i] = i,vis[i] = 0,c[i] = 0;
afir(i,n,1){
l[i] = getpos(l[i]);
r[i] = getpos(r[i]);
int j = l[i],fx = get(j);
while(fx <= r[i]){
fa[fx]++;
c[fx] = i;
fx = get(fa[fx]);
}
}
// fir(i,1,all.size()-1){
// cout << l[i] << " " << r[i] << endl;
// }
int ans = 0;
fir(i,1,all.size()-1){
// cout << c[i] << endl;
if(c[i] && !vis[c[i]]){
ans++;
vis[c[i]] = 1;
}
}
cout << ans << endl;
}
return 0;
}
关于并查集的知识点最主要的就是这几个了.还有类似可持久化并查集之类东西…没学…不会… 这几个知识点里最主要的其实还是要理解并查集到底是如何维护关系的.get和unite的操作是可以变化的.但不变的还是关系传递的方式.还有第四点说到的区间类型的问题并查集也是能很优秀的去解决的.