並查集與最小生成樹

        撥開雲霧見天日……html

一.問題引入ios

        昨晚和老婆深刻交流了下,得出了重大結論:必需要創業。算法

        最後一次給孩子們講課講的這個,不過效果不怎麼理想,一問才知道大一的說只要一涉及到樹就不懂了,我就懷疑講樹哥們怎麼講的(呵呵,純屬調侃,勿怪),居然讓孩子們連入門都沒有,不過在這麼久我也見怪勿怪了,不少老師都只是空談,何談入門,怪不得美國重視啓蒙教育。數組

        我堅信:沒有很差的學生,只有垃圾的教育。話雖這麼說,可是我即使講得再好也沒有多少人會感激我,沒有利益關係,算了,社會就這樣……數據結構

二.理論準備函數

        說以前,我想再說一下,樹圖等只是一種邏輯表示,存在於腦海裏,關鍵要靠存儲結構以及節點間的相互聯繫體現出來,因此咱們常說算法和數據結構,課件算法離不開數據結構的支持,不然就是無本之木、無源之水,兔子的尾巴長不了。性能

        好啦,權當放鬆下心情,咱切入正題吧。優化

        MST的概念相信你們都知道,不過並查集這種數據結構或許有些讀者就有些模糊了,咱先說說並查集。spa

        並查集是一種樹型的數據結構,用於處理一些不相交集合(Disjoint Sets)的合併及查詢問題。經常在使用中以森林來表示。集就是讓每一個元素構成一個單元素的集合,也就是按必定順序將屬於同一組的元素所在的集合合併。3d

        實現通常有以下三個操做:

        初始化:把每一個點所在集合初始化爲其自身,通常來講,這個步驟在每次使用該數據結構時只須要執行一次,不管何種實現方式,時間複雜度均爲O(N)。

        查找:查找元素所在的集合,即根節點。

        合併:將兩個元素所在的集合合併爲一個集合。一般來講,合併以前,應先判斷兩個元素是否屬於同一集合,這可用上面的「查找」操做實現。

        例子:好比親戚關係,食物鏈等,目測NOI上挺多這樣的題目。

        若某個家族人員過於龐大,要判斷兩個是不是親戚,確實還很不容易,給出某個親戚關係圖,求任意給出的兩我的是否具備親戚關係。 規定:x和y是親戚,y和z是親戚,那麼x和z也是親戚。若是x,y是親戚,那麼x的親戚都是y的親戚,y的親戚也都是x的親戚。
有點相似傳遞關係。

        本題的輸入數據量很大,這使得咱們的程序會在輸入中花去很多時間。若是你用Pascal寫程序,能夠用庫函數SetTextBuf爲輸入文件設置緩衝區,這可使輸入過程加快很多。若是你是用C語言的話,就沒必要爲此操心了,系統會自動分配緩衝區。

#include<iostream>
#include<cstring>
#include<cstdio>
#include<cstdlib>
using namespace std;
int father[50002],a,b,m,n,p;
int find(int x){
    if (father[x]!=x) father[x]=find(father[x]);
    return father[x];
}
int main(){
    scanf("%d%d%d",&n,&m,&p);
    for (int i=1;i<=n;i++) father[i]=i;
    for (int i=1;i<=m;i++){
        scanf("%d%d",&a,&b);
        a=find(a),b=find(b);
        father[a]=b;
    }
    for(int i=1;i<=p;i++){
        scanf("%d%d",&a,&b);
        a=find(a);b=find(b);
        if(a==b)printf("Yes");else printf("No");
    }
    return 0;
}

三.並查集

         引入問題:在某個城市裏住着n我的,如今給定關於 n我的的m條信息(即某2我的認識),假設全部認識的人必定屬於同一個單位,請計算該城市最多有多少單位?

        我對最多的理解:如果某兩人不在給出的信息裏,那麼他們不認識,屬於兩個不一樣單位。

        實現方法一:

        n用編號最小的元素標記所在集合;n定義一個數組 set[1..n] ,其中set[i] 表示元素i 所在的集合;

            image

        效率分析,注意下圖中Merge函數的ab是根節點。

               image

        對於「合併操做」,必須搜索所有元素!試試樹結構

        實現方法二:

        每一個集合用一棵「有根樹」表示,定義數組 set[1..n],set[i] = i , 則i表示本集合,並是集合對應樹的根,set[i] = j, j<>i, 則 j 是 i 的父節點.

                      image

           效率分析

                image

        性能有本質改進?n如何避免最壞狀況?

        注意:原文是在merge函數裏先判斷樹的深度,再把小樹掛到大樹上,不過本人認爲這個優化徹底不必,多寫了函數不說,性能也不見得會提升,何況我見的題也不算少,從沒見過誰在並查集裏搞這個,來吧直接路徑壓縮(有人也叫秩壓縮),上文采用的是遞歸,爲便於理解,咱使用非遞歸。

find3(x)
{
      r = x;
      while (set[r] <> r) //循環結束,則找到根節點
          r = set[r];       
      i = x;
      while (i <> r) //本循環修改查找路徑中全部節點
      {   
          j = set[i];
         set[i] = r;
          i = j;
      }
}
        對了,課上有人問我「<>」這個符號啥意思,若是有讀者也不知道啥意思,百度去吧,真心不想在着說了。

        注意:路徑壓縮是在查找的時候壓縮的,每次只會壓縮一個分支,但這以後,再次查找就只須要O(1)時間了,時間節省就在這。

------------------------------------------------------------------------------------------------------------------------------------

        以HDU1232爲例,直接去AC吧

        題目描述:某省調查城鎮交通情況,獲得現有城鎮道路統計表,表中列出了每條道路直接連通的城鎮。省政府「暢通工程」的目標是使全省任何兩個城鎮間均可以實現交通(但不必定有直接的道路相連,只要互相間接經過道路可達便可)。問最少還須要建設多少條道路?

        最赤裸裸的並查集,無話可說~

//課上給大一寫的,1s 
#include <stdio.h>
#include <stdlib.h>
 
 
const int maxn = 1005;
int a[maxn];
 
 
int find(int elem)
{
    int r = elem;
    while(r!=a[r])
       r = a[r];
    //路徑壓縮
    int x = elem;
    int j;
    while(x!=r)
    {
        j = a[x];
        a[x] = r;
        x = j;
    } 
    return r;
}
 
 
void merge(int u, int v)
{
    int i,j;
    int fx = find(u);
    int fy = find(v);
    if(fx!=fy)
        a[fx] = fy; 
}
 
 
int main()
{
    int i,j,k;
    int n,m;
    int u,v;
    while(scanf("%d",&n)&&n) 
    {
        scanf("%d",&m);
        //0號單元不用 
        for(i=0; i<=n; i++)
            a[i] = i;
        for(i=1; i<=m; i++)
        {
            scanf("%d",&u);
            scanf("%d",&v);
            merge(u,v);
        }
        int cnt = 0;
        //不能從0開始,由於a[0] = 0 ,這樣就會多統計一個集合 
        for(i=1; i<=n; i++)
        {
            if(a[i]==i)
                cnt++;
            
        }
        printf("%d\n",cnt-1);
    }
    system("pause");
    return 0;
}

--------------------------------------------------------------------------------------------------------------------------------------------------

        再看一道HDU1272。

        下面的例子,前兩個是符合條件的,可是最後一個卻有兩種方法從5到達8。

                  image

        第一種方法:根據MST性質(您能猜獲得麼),統計頂點數,邊數就是輸入的個數,只要「邊數=頂點數-1」就ok啦。

        一年前寫的代碼,有些醜陋,不過不影響閱讀哈。

//n對點,則必有n條線段, 把全部的起點和終點保存在一維數組中,先排序,計算不重複的點有幾個,即是組成的圖形有幾個點 
//也可使用map 
#include <iostream>
#include <map>
#include <cstring>
using namespace std;
int main()
{
    int i,j,k,T;
    int from, to;
    map <int ,int > mymap;
    int cnt = 0;
    while(cin>>from>>to,~from||~to)
    {
        cnt = 0;//幾組數據 
        while(1)
        {           
            if(!(from||to))       
                break;
            cnt++;
            if(mymap.count(from)==0)
                mymap[from] = to;
            if(mymap.count(to)==0)
                mymap[to] = from;
            cin>>from>>to;
        }
        if(mymap.size()==0)
        {
            puts("Yes"); 
            continue;
        }   
        if(mymap.size()==(cnt+1))
            puts("Yes");
        else
            puts("No");
        mymap.clear();
    }
    return 0;
}

        方法二:並查集實現

        當你輸入的x,y的根節點fx,fy相同時,說明已經可達,再鏈接就是有多餘一條路徑了(環),因此先判斷是否同根就ok了。(迷宮應該只有一個集合,多餘一個就說明不連通,哈哈,說明並查集也能夠判斷是否聯通啊)。

        具體實現留給讀者。

四.最小生成樹

        我們都知道經典算法就是prim和kruskal算法,兩者都是貪心算法。

        先看克魯斯卡爾算法(並查集實現)。

        爲何kruskal能夠用並查集實現?個人理解是這個算法執行的過程就是按照規定一個個連通支合併的過程,使最後只剩一個連通支。

#include <stdio.h>
#include <stdlib.h>
#include <algorithm>
using namespace std;
 
 
const int N = 150;
int m,n,u[N],v[N],w[N],p[N],r[N];
 
 
int cmp(const int i,const int j) 
{
    return w[i] > w[j];
}
 
 
int find(int x) 
{
    return p[x]==x?x:p[x]=find(p[x]);
}
 
 
int kruskal()
{
    int cnt=0,x,y,i,ans=0;
    //n是點數,m是邊數,汝佳那本書上是如此 
    
    //並查集初始化 
    for(i=0;i<n;i++) 
        p[i]=i;
    //邊編號 
    for(i=0;i<m;i++) 
        r[i]=i;
    sort(r,r+m,cmp);
    for(i=0;i<m;i++)
    {
        //取出未加入的邊權最小的邊的編號 
        int e=r[i];
        x=find(u[e]);
        y=find(v[e]);
        if(x!=y) 
        {
            ans += w[e];
            p[x]=y;
            cnt++;
        }
    }
    //找不到最小生成樹 
    if(cnt<n-1) 
        ans=0;
    return ans;
}
 
int main()
{
    int i,ans;
    while(scanf("%d%d",&m,&n)!=EOF&&m)
    {
        for(i=0;i<m;i++)
        {
            scanf("%d%d%d",&u[i],&v[i],&w[i]);
        }
        ans=kruskal();
        if(ans) 
            printf("%d\n",ans);
        else //說明不存在最小生成樹 
            puts("存在最小生成樹!");
    }
    return 0;
}

        再看普利姆算法。

        普里姆算法的基本思想,從連通網N={V,E}中的某一頂點U0出發,選擇與它關聯的具備最小權值的邊(U0,v),將其頂點加入到生成樹的頂點集合U中。之後每一步從一個頂點在U中,而另外一個頂點不在U中的各條邊中選擇權值最小的邊(u,v),把它的頂點加入到集合U中。如此繼續下去,直到網中的全部頂點都加入到生成樹頂點集合U中爲止。

        以NYOJ38爲例,下面這個是之前寫的,沒按數據結構課本上來。

                http://www.cnblogs.com/hxsyl/archive/2012/05/19/2508896.html

        我又實現了下,按嚴蔚敏課本上來實現。

#include<stdio.h>
#include<string.h>
using namespace std;
 
 
int map[505][505];
int v, e;
 
 
int prime()
{
  bool vis[505];
  int dist[505];
    int i,j,sum=0;
  for(i=1;i<=v;i++)
  {
    vis[i]=0;
    //先假設編號爲1的點加入MST 
    dist[i]=map[1][i];
  }
  vis[1]=1;
  for(i=1;i<v;i++)
  {
    int k,min=0x3f3f3f3f;
    for(j=1;j<=v;j++)
    {
      if(!vis[j]&&dist[j]<min)
      {
        min=dist[j];
        k=j;
      }
    }
    /*
    在這也統計下加入了幾天邊,判斷是否構成MST 
    */
    sum+=dist[k];
    vis[k]=1;
    //下面更新已加入最小生成樹的點離其它點的最短距離 
    for(j=1;j<=v;j++)
    {
      if(!vis[j]&&dist[j]>map[k][j])
        dist[j]=map[k][j];
    }
  }
  return sum;
}
int main()
{
  int n;
  int i;
  int waibu;
  scanf("%d", &n);
  while(n--)
  {
    memset(map, 0, sizeof(map));
    scanf("%d %d", &v, &e);
    
    int a, b, c;
    for(i = 0; i< e; i++)
    {
      scanf("%d %d %d", &a, &b, &c);
      map[a][b] = c;
      map[b][a] = c;
    }
    int min = 0x3f3f3f3f;
    for(i = 0; i< v; i++)
    {
      scanf("%d", &waibu);
      if(min > waibu)
        min = waibu;
    }
    printf("%d\n", prime() + min);
  }
  return 0;
}        

        對了,今天查資料時看了什麼破圈法實現普利姆算法,思路是每次找到任意一個圈,去掉權值最大的邊,一直找直到沒有圈。他的那個實現代碼太難看了,能夠說噁心(代碼極度不規則),也沒看懂,有了解的給指點下。

        雖然算法實現了,不過對於算法的正確性本人確實不怎麼了解,不知道如何證實,懂得指點下。

五.結束語

        原本想把MST的擴展應用也加上呢,後來發現加上就太長了,影響閱讀體驗,在此本人鄭重聲明,如果您感受意猶未盡,就請關注本人的下一篇博文,定然不讓您失望,如果您感受閱讀本文後有所收穫,就請您動動手指頭,點下推薦,您的支持是對博主最大的鼓勵;不過如果您發現不當之處,本人深感抱歉,但願沒有誤導到您,還望指正出來,本人會發專文感謝你們的支持與鼓勵,謝謝……

        我在這,你在哪?

地圖圖片

        本文參考了杭電的課件和百度上某些佚名做者的資料,在此表示感謝。

相關文章
相關標籤/搜索