最小生成樹算法:Kruskal算法 Prim算法

定義

對於連通的無向圖G(V,E),若是一個E的無環子集T,能夠鏈接全部節點,而且又具備最小權重,稱樹g(V,T)爲圖G(V,E)的最小生成樹。算法

概念

僞代碼

Kruskal算法和Prim算法均使用貪心策略實現,二者的實現框架可由下列僞代碼表示,首先,是一些敘述時使用的概念。
集合A:某棵最小生成樹的子集。
安全邊: 加入集合A又不會破壞A性質的邊。(在這裏也便是,知足: 加入到A中,且能保證A依舊是某棵最小生成樹的子集 該性質的邊)
  begin
    A初始爲空
    while(A未造成最小生成樹)
        選擇一條安全邊。
        將安全邊加入A。
  end安全

算法正確性證實

主要使用 循環不變式進行證實(這個概念在算法導論中常常用到)
循環不變式:在每遍循環開始以前,A是某棵最小生成樹的子集。
初始化:集合A直接知足循環不變式
保持:算法循環中,選擇安全邊保證了該性質。
終止:當算法終止時,全部邊均屬於某棵最小生成樹,因此算法正確。框架

關於安全邊

預先的一些概念:
切割:無向圖G(V,E)的一個切割(S,V-S)是集合V的一個劃分。
橫跨:若是邊e屬於E,且e的一個端點屬於S,另外一端點屬於V-S,則該邊橫跨切割(S,V - S)
尊重:若是一個E的子集A中不存在橫跨切割(S,V - S)的邊,則稱該切割尊重集合A.
輕量級邊:在橫跨一個切割的全部邊中,權重最小的邊稱爲輕量級邊。
定理:對於無向連通圖G(V,E),A爲E的子集且包含在在某可最小生成樹中,設集合(S,V-S)爲圖G中尊重A的一個切割,若e爲橫跨切割(S,V-S)的一條輕量級邊,則e對於集合A是安全的,即e爲集合A的一條安全邊。
(下面的證實相似讀書筆記,嚴謹的證實請參考算法導論)
證實:假設輕量級邊e兩個端點u,v,(e橫跨切割,因此u,v分別屬於S,V - S),假設A包含在最小生成子樹T(假設T不包含e)中,則 T中存在一條簡單路徑p由u到v,而且T 存在一條邊e';屬於該路徑而且橫跨該切割,如今刪除e‘而且加入e造成另外一個生成樹T',由於權重e <= e'‘因此 權重T' <= T;由於T爲最小生成樹,因此T'也爲最小生成樹。即加入邊e後集合A包含在最小生成樹T'中,即邊e對於集合A爲安全邊。
推論:對於無向連通圖G(V,E),A爲E的子集且包含在在某可最小生成樹中,C(Vc,Ec)爲森林G‘(V,A)(包含G全部頂點,以及集合A中邊,由A的定義,G'無環,且包含多個連通份量,由此構成森林G')中的一個連通份量,若是e鏈接C和其餘連通份量的一條輕量級邊,則e對集合A是安全的。
證實:容易知道,切割(Vc,V - Vc)尊重A,由定理可得推論正確性。
Kruskal算法:集合A爲森林。尋找安全邊的方式是,權重最小且鏈接兩個連通份量(樹)的邊。
Prim算法:集合A爲樹。尋找安全邊的方式爲,鏈接A與A以外節點的權重最小的邊。code

Kruskal算法

主要使用並查集來查詢集合選擇的邊是否屬於同一連通份量。
僞代碼
    A初始爲空
    創建並查集
    按照權重對邊進行升序排序。
    按順序考察每條邊:
        若是邊端點分別屬於不一樣連通份量,加入該邊。排序

代碼實現

主要實現了並查集(按秩合併 和 路徑壓縮)隊列

int a[101];
int rk[101];
void init_set()
{
    for(int i = 1; i <= 100; ++i)
    {
        a[i] = i;
        rk[i] = 1;
    }
}
int find_set(int x)
{
    int p = x;
    while(a[p] != p)
    {
        p = a[p];
    }
    int now = x;
    int tmp = 0;
    while(now != p)
    {
        tmp = a[now];
        a[now] = p;
        now = tmp;
    }
    return p;
}
void merge(int xp ,int yp)
{
    if(rk[xp] > rk[yp])
    {
        a[yp] = xp;
    }
    else 
    {
        a[xp] = yp;
        if(rk[xp] == rk[yp])
        {
            ++rk[yp];
        }
    }
}
struct Nod
{
    int x,y,w;//分別表示邊的端點x,y和邊的權重w.
    Nod():x(0),y(0),w(0){}
    bool operator < (const Nod& tmp)const
    {
        return w < tmp.w;
    }
};
int kruskal()
{
    init_set();
        int e = n*n;//邊的數量;
    sort(nod,nod + e);
    int s1,s2;
    for(int i = 1; i <= e; ++i)
    {
        s1 = find_set(nod[i].x);
        s2 = find_set(nod[i].y);
        if(s1 != s2)
        {
            merge(s1,s2);
        }
    }
    return ret;
}

時間複雜度分析

    O(ElgV)
    詳細分析暫時跳過:it

Prim 算法

#define INF 0x7fffffff
int in[101];
struct Nod
{
    int id,key;//分別爲節點的序號和集合A到該節點權重最小的邊的權重。
    Nod():id(0),key(0){}
    bool operator <(const Nod& tmp)const
    {
        return key > tmp.key;
    }
};
int key[101];//集合A到該節點的邊的權重,不存在該邊,設爲無窮大。
void prim(int r)
{
    for(int i = 1; i <= n; ++i)
    {
        key[i] = INF;
        in[i] = 0;
    }
        
    Nod tmp;
    key[r] = 0;
    tmp.id = r;
    tmp.key = 0;
    priority_queue<Nod> q;//使用優先隊列維護待選擇的節點。
    q.push(tmp);
    while(!q.empty())//操做1,取點
    {
        tmp = q.top();
        q.pop();
        if(key[tmp.id] < tmp.key)continue;
        int id = tmp.id;
        in[id] = 1;//標記爲1,,標識屬於最小生成樹集合;

                 //操做2,考察邊。
        for(int i = 1; i <= n; ++i)//邊數較少可以使用鄰接邊實現,這裏使用矩陣實現,
        {
            if(!in[i] && a[id][i] < key[i])
            {
                key[i] = a[id][i];
                tmp.id =i;
                tmp.key = key[i];
                q.push(tmp);
            }
        }
    }
}

時間複雜度分析

  
操做1:使用優先隊列實現,時間複雜度爲lg(V)
操做2:考察全部邊,時間複雜度O(E)
總的時間複雜度爲:O(Elg(V));
主要參考算法導論,若有錯誤,懇請指正io

相關文章
相關標籤/搜索