並查集與路徑壓縮

引子
如今來看這樣一個經典問題:
親戚
若某個家族人員過於龐大,要判斷兩個是不是親戚,確實還很不容易,如今給出某個親戚關係圖,求任意給出的兩我的是否具備親戚關係
規定: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->12->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

相關文章
相關標籤/搜索