并查集入门到再入门

初进ACM的话,并查集考点不会太难。(鄙人愚见,若遇变态题,勿喷)
所以知道这个概念后,知识点没深入过,一直感觉是初学状态,最近又遇到这类基础题,便写此文,权当再入门一遍。

  • 并查集概念(来自百度):

并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。常常在使用中以森林来表示。集就是让每个元素构成一个单元素的集合,也就是按一定顺序将属于同一组的元素所在的集合合并。

  • 说说我自己的理解:

初学阶段遇到的题型:几个离散点,给他们建关系,然后问些基础问题。比如
判断两个点是否有关系(连通)?(基础)
还有几个点是离散的?(经典例题:畅通工程)
判断其中是否构成回路?(经典例题:小希的迷宫)
找一个根,问这个根上面有多少节点?(基础)
乱七八糟的关系(中难题:食物链)
(文章最后给例题链接)

  • 并查集的复杂度:O(1)

比如说,我们要在一个无重复数据的数组中寻找一个指定的元素,那么最简单的方法就是直接for循环一遍暴力查找即可。暴力的时间复杂度为O(n)。
再比如,如何判断一个简单无向图是否为连通图(图一般会以二元组序列形式给出)dfs/bfs直接搜索(空间复杂度为O(n*n) (邻接矩阵存图)、O(n)(邻接表存图,但是vector会额外占用大量时间),时间复杂度为O(n),因为如果联通,每个点都会被搜索到有且仅有一遍)

当然有特殊情况,并查集的查询操作最坏情况下的时间复杂度为O(n),其中 n 为总元素个数。
最坏情况发生时,每次合并对应到森林上都是一个点连到一条链的一端。此时如果每次都查询链的最底端,也就是最远离根的位置的元素时,复杂度便是O(n)了。
这种情况不太会出现,没意思的,还考你并查集吗?

  • 并查集的三个基本play:

先建些基本变量

int n;//我们以n个点建树
const int maxn=1e5+5;//边界(最大值)
int f[maxn],sum[maxn];//f[maxn]作根,sum[maxn]用于计数
//问题:“找一个根,问这个根上面有多少节点?”会用到sum[maxn]

1.建立并查集(初始化):

因为一开始都是离散点,所以根为自己,sum都为1.

void init(){
    for(int i=1;i<=n;i++){
        f[i]=i;
        sum[i]=1;
    }
}

2.合并操作(建立关系)

并查集的合并操作需要用到查询操作的结果。合并两个元素所在的集合,需要首先求出两个元素所在集合的代表元素,也就是节点所在有根树的根节点。接下来将其中一个根节点的父亲设置为另一个根节点。这样我们就把两棵有根树合并成一棵了。

当然,如果x和y本来就同在一棵树上,那就不需要合并这个操作了。而判断是否在同一棵树上的依据是是否有同一个根。

我们设find(x)表示x所在树的根节点,那么合并操作代码如下:

void merge(int x,int y){
	int a=find(x),b=find(y);
	if(a!=b){
		f[a]=b;		
	}
}

这样操作后,a指向b,a就是b的一个节点。
当然调试的时候有点烦,因为每次合并后,最后所有的节点的f[i]值很乱。(都是找临近的节点作根)

所有可以每次都会直接指向同一个根。代码如下:

void merge(int x,int y){
    if(find(y)==find(y))
        return;
    f[find(x)]=find(y);
}

这样做也有点弊端,如果输入的关系组数过多,每次都要找最终的根,很浪费时间,而且这波操作后,都变成了只有一层的树。不推荐用。

当然小编写这类代码有自己的习惯,把第一类代码改一下,较小的树作临近的根,(虽然没什么大用,至少调试的时候清晰一点,而且如果要计数也方便)

void merge(int x,int y){
	int a=find(x),b=find(y);
	if(a!=b){
		int mi=min(a,b),mx=max(a,b);
		f[b]=a;		//较大的数指向根 
		sum[mi] += sum[mx];		//根的sum加上节点的sum 
	}
}

这纯属自己个人习惯,常用的还是第一类。

3.查询操作(找根结点)

简易版:

int find(int x){
    if(f[x]==x)//如果已经是根节点
        return x;
    return f[x]=find(f[x]); 
}

简化版:

int find(int x){
    return x==f[x]?x:f[x]=find(f[x]);
}

三目运算符不难理解,我平时也只用第二类。

  • 例题:

涉及到计数问题
题目大意:
n个离散点,m组关系后,找第k个点和它有关系的有多少点(包括自己)

#include <bits/stdc++.h>
#pragma GCC optimize(2)
using namespace std;
typedef long long ll;
const int maxn = 1e5+5;
int f[maxn],sum[maxn];
int find(int x){
    return x==f[x]?x:f[x]=find(f[x]);
}
int main(){
    int n,m,k,a,b;
    cin>>n>>m>>k;
    for(int i=1;i<=n;++i) f[i]=i, sum[i]=1;
    //初始化,每个节点都指向自己,且个数sum都为一个 
    while(m--){
        cin>>a>>b;
        if(find(a)!=find(b)){
        	//这里统一 :较小的数为根 
            int mi = min(find(a),find(b)) , mx = max(find(a),find(b));
            sum[mi] += sum[mx];		//根的sum加上节点的sum 
            f[mx] = mi;				//较大的数指向根 
        }
    }
    cout<<sum[find(k)]<<endl;
    return 0;
}

当然还有入门题,有个博客已经汇总得不错了,下面链接
入门例题

还有一道食物链的中难题,量力而行,切勿思考过多而掉发
食物链

这里是我ac的代码,这题挺训练思维的

#include<stdio.h>
#include<iostream>
#include<string>
#include<algorithm>
#define LIMIT 150001
using namespace std;
int a[LIMIT];
int find(int x){
	return a[x]==x?x:a[x]=find(a[x]);
}
void merge(int x,int y){
	int fx=find(x),fy=find(y);
	if(fx!=fy){
		a[fx]=fy;
	}
}
int main(){
	int n,k,m,x,y;
	int count=0;
	scanf("%d %d",&n,&k);
	for(int i=1;i<=3*n;i++)     a[i]=i;    //初始化 
	for(int z=0;z<k;z++){
		scanf("%d %d %d",&m,&x,&y);
		if(x>n||y>n){
			count++;
			continue;
		}
		if(m==1){
			if(find(x)==find(n+y)||find(y)==find(n+x)){
				count++;
				continue;
			}
			else{
				merge(x,y);
				merge(x+n,y+n);
				merge(x+2*n,y+2*n);
			}
		}
		else if(m==2){
			if(x==y){
				count++;
				continue;
			}
			else if(find(x)==find(y)||find(2*n+y)==find(x)){
				count++;
				continue;
			}
			else{
				merge(x,y+n);
				merge(x+n,y+2*n);
				merge(x+2*n,y);
			}
		}
	}
	printf("%d",count);
}
  • 超越入门

下面就不是小白专场了,说实话我自己也正在研究。先附个链接
并查集的一些基本概念以及基本操作(初始化,合并,查询等操作)
这篇博客中涉及并查集最坏情况下如何优化,基础概念较少,但对并查集分析挺透彻的。

量力而行吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章