並查集入門到再入門

初進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);
}
  • 超越入門

下面就不是小白專場了,說實話我自己也正在研究。先附個鏈接
並查集的一些基本概念以及基本操作(初始化,合併,查詢等操作)
這篇博客中涉及並查集最壞情況下如何優化,基礎概念較少,但對並查集分析挺透徹的。

量力而行吧。

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