问题引入
- 我们常常会遇到这样的问题:
有一群人,他们有一些人是认识的,假设A认识B,B认识C,那么称A,B,C是属于一个帮派,那么如果已知许多的关系,那么这里面有多少个帮派? - 问题表述很简单,那么我们应该怎么去解决这样一个问题呢?如果用平凡的解法,不借助其他数据结构,复杂度比较大,使用并查集可以大大减小时间复杂度,思路也更加清晰
结构
并查集的主要思想是分组,开始的时候所有人自己是一组,随着条件的加入,将不同的人分到一组去,那么如何去实现呢?
- 首先使用一个数组,开始的时候set[i] = i,也就是自己和自己一组,如果得到一个信息,说1号和2号是一个组的,那么set[1]=set[2]也就是把2所在的组号赋值给1,或者反过来一样,这就是合并操作;至于查找操作,并查集数组是一种树状的逻辑结构,只需要递归查找x == set[x]的根节点就能够找到这一个数字是在哪一个集合里面,可以画图辅助理解
int FIND_PATH(int x){
if(x == set[x]) return x;
return FIND_PATH(set[x]);
}
void UNION(int x,int y){
x = FIND_PATH(x);
y = FIND_PATH(y);
if(x !=y) set[x] = set[y];
}
路径压缩
递归的好处是书写方便,但是带来比较大的时间消耗,我们可以在查找的过程中顺便修改父节点的set值,让每一个元素都直接跟祖先节点,这样可以大大减小时间
int FIND_PATH(int x){
if(x == set[x]) return x;
return set[x] = FIND_PATH(set[x]);
}
只需要在递归的每一层顺便修改set数组即可
按秩合并
树的层数影响搜索的效率,所以使用一个rank数组记录层数,按照两棵树的rank大小来判断将谁合并到谁那里去,在这里应该把矮的树合并到高树中,这样可以减少树的层数,假设相反,那么高树势必拉长矮树,这样层数增加,查找效率也就降低了
void UNION(int x,int y){
x = FIND_PATH(x);
y = FIND_PATH(y);
if(x == y) return;
if(rank[x] > rank[y]) set[y] = set[x];
else{
set[x] = set[y];
if(rank[x] == rank[y]) rank[y]++;
}
}
例题
首先是模板题
洛谷1551
模板
#include <iostream>
using namespace std;
const int MAXN = 2e5+100;
int set[MAXN];
int rank[MAXN];
int FIND_PATH(int x){
if(x == set[x]) return x;
return set[x] = FIND_PATH(set[x]);
}
void UNION(int x,int y){
x = FIND_PATH(x);
y = FIND_PATH(y);
if(x == y) return;
if(rank[x] > rank[y]) set[y] = set[x];
else{
set[x] = set[y];
if(rank[x] == rank[y]) rank[y]++;
}
}
int main(){
int n,m,p,x,y;
cin>>n>>m>>p;
for(int i=1;i<=n;i++) set[i] = i;
for(int i=0;i<m;i++){
cin>>x>>y;
UNION(x,y);
}for(int i=0;i<p;i++){
cin>>x>>y;
if(FIND_PATH(x) == FIND_PATH(y)) cout<<"Yes";
else cout<<"No";
cout<<endl;
}
return 0;
}
poj2236
题目大意:n台电脑,给定距离d,处于距离d以内的电脑之间能联系,开始的时候电脑都是坏的,每次指令O可以修复一台电脑,指令S询问两台电脑之间能否联系
- 思路是并查集,每次O指令将距离之内的好的电脑和现在修好的放在一个集合,S指令直接查询即可
- 合并操作有些费时,但是时间放得很宽
#include <iostream>
using namespace std;
const int MAXN = 2e5+100;
int set[MAXN];
int rank[MAXN];
struct NODE{
int x,y;
}node[MAXN];
int FIND_PATH(int x){
if(x == set[x]) return x;
return set[x] = FIND_PATH(set[x]);
}
void UNION(int x,int y){
x = FIND_PATH(x);
y = FIND_PATH(y);
if(x == y) return;
if(rank[x] > rank[y]) set[y] = set[x];
else{
set[x] = set[y];
if(rank[x] == rank[y]) rank[y]++;
}
}
int dis(NODE X,NODE Y){
int dx = X.x - Y.x;
int dy = X.y - Y.y;
return (dx * dx + dy * dy);
}
int vis[MAXN];
int main(){
char c;
int n,d,m,p;
cin>>n>>d;
for(int i=1;i<=n;i++) set[i] = i;
for(int i=1;i<=n;i++) cin>>node[i].x>>node[i].y;
while(cin>>c){
if(c == 'O'){
cin>>m;
for(int i=1;i<=n;i++){
if(vis[i]&&i!=m&&dis(node[m],node[i])<=d*d){
UNION(i,m);
}
}
vis[m] = 1;
}else if(c == 'S'){
cin>>m>>p;
if(FIND_PATH(m) == FIND_PATH(p)) cout<<"SUCCESS"<<endl;
else cout<<"FAIL"<<endl;
}
}
return 0;
}
poj1611
模板题
#include <iostream>
using namespace std;
const int MAXN = 2e5+100;
int set[MAXN];
int rank[MAXN];
struct NODE{
int x,y;
}node[MAXN];
int FIND_PATH(int x){
if(x == set[x]) return x;
return set[x] = FIND_PATH(set[x]);
}
void UNION(int x,int y){
x = FIND_PATH(x);
y = FIND_PATH(y);
if(x == y) return;
if(rank[x] > rank[y]) set[y] = set[x];
else{
set[x] = set[y];
if(rank[x] == rank[y]) rank[y]++;
}
}
int main(){
int n,m,p,x,y;
while(cin>>n>>m){
if(n == 0&&m == 0) break;
for(int i=0;i<n;i++) set[i] = i;
while(m--){
cin>>p>>x;
for(int i=1;i<p;i++){
cin>>y;
UNION(x,y);
}
}
int ans = 0;
for(int i=0;i<n;i++){
if(FIND_PATH(i) == FIND_PATH(0)) ans++;
}cout<<ans<<endl;
}
return 0;
}
- 注意一共只有A、B、C三种动物,且有关系A吃B,B吃C,C吃A
- 那么这题可以考虑使用三个数组记录,但是还有另一种考虑方法就是可以将数组开大一些,用x,x+n,x+2*n这样三个区间去表示这三种动物的集合
- 如果两种动物属于同类动物,那么对应的x,x+n,x+2n、y,y+n,y+2n应该分别处于相同集合(这里的意思是x和y可能处于不同的区间,比如x和y都是A类动物);谎话的判断是x和y不在同一个集合,也就是x和y+n或者x和y+2*n处于同一个集合
- 如果两种动物存在x吃y的关系,那么可能是A吃B,也可能是B吃C,还可能是C吃A,所以要把x、y处于这样三个集合的部分都进行合并操作;谎话的判断是x和y处于同一个集合或者y吃x(注意这里)
#include <iostream>
#include <cstdio>
using namespace std;
const int MAXN = 2e5+100;
int set[MAXN];
int rank[MAXN];
struct NODE{
int x,y;
}node[MAXN];
int FIND_PATH(int x){
if(x == set[x]) return x;
return set[x] = FIND_PATH(set[x]);
}
void UNION(int x,int y){
x = FIND_PATH(x);
y = FIND_PATH(y);
if(x == y) return;
if(rank[x] > rank[y]) set[y] = set[x];
else{
set[x] = set[y];
if(rank[x] == rank[y]) rank[y]++;
}
}
int main(){
int n,k,op,x,y;
cin>>n>>k;
for(int i=1;i<=3*n;i++) set[i] = i;
int ans = 0;
while(k--){
scanf("%d%d%d",&op,&x,&y);
if(x<=0||y<=0||x>n||y>n){
ans++;
continue;
}
if(op == 2&&x == y){
ans++;
continue;
}
if(op == 1){
if(FIND_PATH(x) == FIND_PATH(y+n)||FIND_PATH(x) == FIND_PATH(y+2*n)){
ans++;
continue;
}
UNION(x,y);
UNION(x+n,y+n);
UNION(x+2*n,y+n*2);
}else if(op == 2){
if(FIND_PATH(x) == FIND_PATH(y)||FIND_PATH(x) == FIND_PATH(y + 2*n)){
ans++;
continue;
}
UNION(x,y+n);
UNION(x+n,y+2*n);
UNION(x+2*n,y);
}
}
cout<<ans;
return 0;
}
hdu1272
这个题乍一看好像最小生成树,实际上有点类似Kruskal,按照题目要求,不能出现环,并且图应该是连通图,如果出现环,那么一定有一条边他的两个顶点处于同一集合,用并查集解决;如果图不是连通图,那么图应该可以分成两个集合,也就是说图有两个根节点,这可以通过遍历set数组得到
#include <iostream>
#include <cstring>
#include <cstdio>
using namespace std;
const int MAXN = 2e5+100;
int set[MAXN];
int RANK[MAXN];
int vis[MAXN];
struct NODE{
int x,y;
}node[MAXN];
int FIND_PATH(int x){
if(x == set[x]) return x;
return set[x] = FIND_PATH(set[x]);
}
void UNION(int x,int y){
x = FIND_PATH(x);
y = FIND_PATH(y);
if(x == y) return;
if(RANK[x] > RANK[y]) set[y] = set[x];
else{
set[x] = set[y];
if(RANK[x] == RANK[y]) RANK[y]++;
}
}
int main(){
int n,m;
for(int i=1;i<=100000;i++) set[i] = i;
bool flag = true;
int num;
int cnt = 0;
while(scanf("%d%d",&n,&m)){
if(n == -1&&m == -1) break;
if(n == 0&&m == 0){
for(int i=1;i<=100000;i++){
if(set[i] == i&&vis[i]) cnt++;
}if(cnt>1) flag = false;
for(int i=1;i<=100000;i++) set[i] = i;
for(int i=1;i<=100000;i++) vis[i] = 0;
cnt = 0;
if(flag) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
flag = true;
}else{
if(FIND_PATH(n) == FIND_PATH(m)) flag=false;
vis[n] = vis[m] = 1;
UNION(n,m);
}
}
return 0;
}