1. 程式人生 > >2019.9.17 初級資料結構——並查集及其應用

2019.9.17 初級資料結構——並查集及其應用

一、並查集基礎

(一)引入

我們先來看一個問題。

某學校有N個學生,形成M個俱樂部。每個俱樂部裡的學生有著一定相似的興趣愛好,形成一個朋友圈。一個學生可以同時屬於若干個不同的俱樂部。根據“我的朋友的朋友也是我的朋友”這個推論可以得出,如果A和B是朋友,且B和C是朋友,則A和C也是朋友。請編寫程式計算最大朋友圈中有多少人。

輸入格式:
輸入的第一行包含兩個正整數N(≤30000)和M(≤1000),分別代表學校的學生總數和俱樂部的個數。後面的M行每行按以下格式給出1個俱樂部的資訊,其中學生從1~N編號:

第i個俱樂部的人數Mi(空格)學生1(空格)學生2 … 學生Mi

輸出格式:
輸出給出一個整數,表示在最大朋友圈中有多少人。

輸入樣例:
7 4
3 1 2 3
2 1 4
3 5 6 7
1 6
輸出樣例:
4

我們把這個題抽象化:

現在有n個數,它們按照一定規律合併,形成一些集合,求最大集合的元素個數。

這個合併規律如下:

(1)同一個俱樂部的所有人屬於同一個集合;

(2)若兩個俱樂部包含同一個人,則這兩個俱樂部屬於同一個集合。

按照正常做法,我們對於每一個人按照上述規律掃描剩餘的人,運行復雜度O(n^很大)。

這個方法顯然過不了。

我們重新分析這個題目,主要難點有兩個:

第一,對於第一條,同一個俱樂部的所有人如何儲存它們所在同一個集合內。

這個很好解決,我們再開一個book陣列,對於每一個人i,假設它在cnt集合內,我們標記book[i]=cnt。

如果這樣標記,對於第二個合併規律,我們掃描另一個俱樂部的所有人,我們把每一個book[i]都置成要合併到的俱樂部,總複雜度O(nm)。

但這樣還是過不了,所以我們考慮把第二個合併步驟的複雜度優化到O(1),也就是說我們只需要修改一個數就可以修改整個集合。

對於這樣的修改方式,我們可以想到一種資料結構——樹。

利用樹儲存每個集合有兩個好處:

第一,每棵樹只有一個樹根(也就是查詢樹根就可以查詢整棵樹)

第二,兩棵樹合併只需要把一棵樹的樹根接到另一棵上(所以修改的複雜度是O(1))。

大概就是上面這麼合併

也就是說我們可以用樹優化查詢和合並集合的過程,其本質就是對樹之間的處理。

又因為,這一類題的根本步驟就是查詢與合併,所以我們把這樣的資料結構叫做並查集。


 

(二)並查集的基本操作

剛才我們已經知道,並查集中合併兩個元素實際上是合併他們所在樹的祖先。

為了查詢這兩個點是否在一棵樹上,我們首先要找到他們兩個點的祖先分別是誰,並判斷他們的祖先是否是同一個點即可。

但是因為樹會退化成一條鏈,所以我們在查詢祖先時要把下圖的第一個樹變成第二個樹,這是後話。

第二個圖的意思就是所有的點都連到根節點上。

我們暫且不提怎麼查詢一個點的祖先,在我們查詢兩個點祖先之後,判斷他們的祖先是不是同一個點,如果不是同一個點合併即可。

所以我們用fa[x]表示x的父親,則程式碼如下:

void u(int a,int b)
{
	int c=f(a),d=f(b);
	if(fa[c]!=fa[d])
	{
		fa[min(c,d)]=fa[max(c,d)];
		return;
	}
}

一定要注意的是,絕對不能讓fa[min(a,b]直接等於fa[max(a,b)],因為這樣只合並了兩個點,而不是其所在的樹。

同時,注意合併時要按照一定的大小順序,所以這裡是按小往大合的。

所以這是合併兩個點的方法。

關於查詢祖先的方法,有一個非常絕妙的方法:

int f(int o)
{
	if(fa[o]==o)return o;
	return fa[o]=f(fa[o]);
}

第一行非常容易理解,如果o的祖先時自己則它自己就是這棵樹的祖先。

第二行我們把它分解成兩部分:

(1)f(fa[o])找到fa[o]的祖先。

(2)return fa[o]=f(fa[o]) 將o的父親置為其父親的祖先

也就是說 我們成功把一棵近似於一條鏈的樹 弄成了一棵最多隻有三層的樹。

這個過程換句話說是這樣:

(1)遞迴地找點o每一級祖先o',直到找到它真正的祖先o1;

(2)回溯,對於每一個o',將fa[o]置為o1.

也就是說 通過這個步驟,這棵樹的每一個點都直接連線到了祖先上

所以回到這章開始的部分,對於那兩棵樹之間的變化,有一種方法就是上面說的更改每個點與祖先的連線方式,這種方式叫作路徑壓縮。

其實還有一個稍微好實現一點的方法,對於每個樹根,我們記錄它所在的樹的高度,把這個高度叫作這棵樹的秩。

對於每個合併操作(不是路徑壓縮時的查詢操作),我們把秩小的樹合到秩大的樹上,這樣可以保證合出來的新樹的秩不大於原來任何一棵樹。

若兩棵樹的秩一樣,則新樹的秩是原樹的秩+1.

這種方法也叫按秩合併。

最後記得一開始初始化,令fa[x]=x。

上個板子:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int fa[100050];
int f(int o)
{
	if(fa[o]==o)return o;
	return fa[o]=f(fa[o]);
}
void u(int a,int b)
{
	int c=f(a),d=f(b);
	if(c!=d)fa[min(c,d)]=fa[max(c,d)];
	return;
}
int main()
{
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)fa[i]=i;
	for(int i=1;i<=m;i++)
	{
		int x,y,opt;
		scanf("%d%d%d",&x,&y,&opt);
		if(!opt)u(x,y);
		else
		{
			if(f(x)!=f(y))puts("NO");
			else puts("YES");
		}
	}
	return 0;
}

  高階用法待填坑。

&n