引子:
如今來看這樣一個經典問題:
親戚
若某個家族人員過於龐大,要判斷兩個是不是親戚,確實還很不容易,如今給出某個親戚關係圖,求任意給出的兩我的是否具備親戚關係。
規定:x和y是親戚,y和z是親戚,那麼x和z也是親戚。若是x,y是親戚,那麼x的親戚都是y的親戚,y的親戚也都是x的親戚。ios
怎麼作?
深搜?廣搜?效率過低。
鄰接矩陣?哇MLE(爆內存)!
因而咱們有一種新的方法——並查集。markdown
分析一下這道題,咱們發現題目的核心在於判斷兩個元素的關係(是否處於一個集合),輸入給出n對元素兩兩相連。看來重點在於如何建出整個集合(多是多個)來便於查詢。ui
並查集的做用:判斷兩個集合(點)之間的關係。spa
並查集的結構:每個集合中的元素指向那個集合的表明元素(father)。如此當判斷A和B是否在一個集合中的時候,咱們只須要查詢他們的father是不是同一個即可得出結論(後來有些變更,但大體意思是這樣的)。code
並查集的初始化:
有n個元素,開始咱們將它的father 指向它本身,也就是說每一個集合中只有一個元素。遞歸
並查集的合併:
如今咱們得知A和B是親戚,那麼咱們如何合併這兩個集合哪?
答案是找到他們各自的father,將其中一個father指向另外一個(father[fathera] = fatherb),這樣咱們便完成了並查集的合併。內存
那麼出現了一個問題,請看下例:
已知fa[1] = 2, fa[2] = 3, fa[3] = 4,如今咱們得知2和6有關係,那麼根據上述步驟fa[2] = 6,如此完成了合併。fa (father)
我如今想知道2和4是否有關係,判斷獲得fa[2] != fa[4],沒有關係!但事實上…
看來咱們缺乏了一個步驟——將該集合的其餘元素的fa也更新爲新的father。
由此便引出了並查集的核心之一——表明元素的選擇。string
根據上文,咱們知道每個集合的元素都指向該集合的表明元素,那麼該表明元素能夠隨意取嗎?
很顯然,天然是不能夠。那麼表明元素須要有些什麼要求?
根據fa[i] = j得出i的父親是j,意味着i有父親,可是一個有父親的元素怎麼能表明整個集合哪?至少得是它的父親表明啊。
一層一層向上,咱們便能找到一個祖先,它沒有父親。它即是那個長者,它能夠表明整個集合的元素。
獲得結論:表明元素需知足:fa[i] == i(初始化後沒有父親,可是有孩子…)it
那好,一個元素中的孩子與另外一個元素的孩子有關係,合併兩個集合。咱們只須要一層一層找父親,最終將一個的祖先指向另外一個,大功告成!io
再看上文的例子:咱們找到2的祖先4,fa[4] = 6,再判斷,2的祖先是…找到6,4的祖先…是6,那麼它們有同一個祖先,看來是在一個集合裏。
再仔細讀一遍流程,咱們會發如今查找和合並的時候咱們找了i的兩次祖先,可是明明在找祖先的過程當中咱們都遍歷到了i的長輩們,還要作兩次,感受好像作了無用功。
若是你並不這樣認爲,我看下面一個例子。
10000000祖先是1,1000000的父親是1000000-1,對於該集合的元素n,它的父親是n-1(n != 1)。我想找到1000000的祖先,使它與a合併。那麼我一層一層的爬…爬了1000000-1次,終於找到了1,因而咱們將1000000的集合與a的集合愉快地合併了。
我如今忽然傻了,記不住a和1000000的關係了。因而我又開始找父親了…又是1000000-1次尋找,我終於發現它們倆是一個集合。1s中你能操做1000000-1次的尋找幾回?哇TLE了!
由此引出了並查集的真正核心——路徑壓縮。
咱們最開始的尋找(find_fa)的代碼應該是這樣的
while(fa[i] != i)
{ i = fa[i]; }
最終的i即是祖先。
可是若是咱們壓縮一下路徑,像這樣:
int find_fa(int a){
if(a != fa[a]) fa[a] = find_fa(fa[a]);
return fa[a];
}
這段代碼什麼意思?
效果是這樣的:
原:n->n-1->n-2->,,,->2->1;
現:n->1, n-1->1, n-2->1… 2->1;
咱們遞歸尋找,找到後回溯更新全部遍歷到的節點的父親,將它們指向祖先,一共是2*n次操做。若是不這麼操做,改用樸素法,詢問全部元素的祖先咱們須要進行(1+2+…+n)次操做,即(1+n) * n/2次操做,而壓縮完路徑,咱們只須要n次操做。兩種查詢的效率根本不在一個數量級上。
那麼開篇的那道題,是一個很好的板子題。
源代碼以下:
#include <iostream>
#include <cstdio>
using namespace std;
const int maxn = 5005;
int n ,m, p;
int qi[maxn];
int rel(int a){
if(a != qi[a]) qi[a] = rel(qi[a]);
return qi[a];
}
int main(){
//freopen("test.in", "r", stdin);
scanf("%d%d%d", &n, &m, &p);
for(int i = 1; i != n+1; ++i)
qi[i] = i;//初始化
for(int i = 0; i != m; ++i){
int c1, c2;
scanf("%d%d", &c1, &c2);
c1 = rel(c1);//找c1祖先
c2 = rel(c2);//找c2祖先
qi[c2] = c1;//c2祖先指向c1祖先
}
for(int i = 0; i != p; ++i){
int c1, c2;
scanf("%d%d", &c1, &c2);
if(rel(c1) == rel(c2))//一個祖先
cout << "Yes" << endl;
else cout << "No" << endl;
}
return 0;
}
由此並查集便完成了。
箜瑟_qi 2017.04.09 23:48