1. 程式人生 > >第七章 圖(最小生成樹之prime演算法和 kruskal演算法)

第七章 圖(最小生成樹之prime演算法和 kruskal演算法)

最小生成樹

所謂最小生成樹,就是在一個具有N個頂點的帶權連通圖G中,如果存在某個子圖G’,其包含了圖G中的所有頂點和一部分邊,且不形成迴路,並且子圖G’的各邊權值之和最小,則稱G’為圖G的最小生成樹。
由定義我們可得知最小生成樹的三個性質:
•最小生成樹不能有迴路。
•最小生成樹可能是一個,也可能是多個。
•最小生成樹邊的個數等於頂點的個數減一。

prime演算法

以下面的圖為例,實現prime演算法
這裡寫圖片描述

#include<iostream>
#include<cstring> using namespace std; const int inf=10000; int graph[4][4]={ {inf,6,1,inf}, {6,inf,5,3}, {1,5,inf,4}, {inf,3,4,inf} }; bool vis[4]; int m=4; int dis[100]; int prime(int cur) { int index; int
sum = 0; memset(vis, false, sizeof(vis)); vis[cur] = true;//最小生成樹的起點標記訪問過 for(int i = 0; i < m; i ++){ dis[i] = graph[cur][i]; } //cur點已經訪問過,剩下的結點數目是m-1 for(int i = 1; i < m; i ++){ int minmum=inf; for(int j = 0; j < m; j ++){ if(!vis[j] && dis[j] < minmum){ minmum = dis[j]; index = j; } } vis[index] = true
; sum += minmum;//求最小生成樹的最小邊權之和 //更新dis陣列,對index點來說,與之相連的邊和對應的以前的邊 //比較,選擇較短的邊。比如開始和點2相連的所有邊是6,inf,5,3 //index更新為點4後,與4相連的所有邊是inf,3,4,inf,原來的dis //是6,inf,5,3更新成6,3,4,3 for(int j = 0; j < m; j ++){ if(!vis[j] && dis[j] > graph[index][j]){ dis[j] = graph[index][j];//更新dis陣列 } } } return sum; } int main(){ cout<<prime(1)<<endl; return 0; }

kruskal演算法

講kruskal演算法之前,先講並查集

並查集

並查集根據其名字幹兩件事:
1.把元素a所在的集合和元素b所在的集合合併為一個集合。
2.查元素a和元素b是否屬於同一個集合。

下面給出程式碼(含有解釋),該程式碼合併集合的方法是根據樹的深度,把深度小的樹(集合)合併到深度大的(集合)上,如下示例。
這裡寫圖片描述

/*
    並查集思路。
    並查集演算法的思想,結合了樹的思想。
    剛開始給出n個元素,這n個元素可以看成n棵樹。對於其中一個元素a,
    如果另一個元素b和a屬於同一個集合,那麼我們可以把b當做a的父母,
    或者把a當做b的父母(起初,每個元素的父母是他自己),如果元素c
    和a,b仍然屬於同一個集合,那麼我們可以把a,b中任一個當做c的父母,
    或者把c當做a或b的父母,以此類推。這樣集合用一棵樹表示了。不同
    的集合表示成了不同棵樹。如果我們想把不同集合合併,我們只需要將
    一個集合中任一個元素當做另一個集合的父母即可(即在兩棵樹直接添
    加了一條邊,把兩棵樹連在了一起)。如果我們想查兩個元素d,e是否
    屬於同一個集合,我們只需要一直遞歸向上查d,e的父母(即他們分別
    所屬的樹根)(請記得起初的時候每個元素的父母是他自己,遞迴到最
    後一定能查出一個值),如果他們的父母相同,則他們屬於同一個集合,
    否則,不屬於同一個集合。
    上面的描述中,還有一點補充,給每一個元素定個級別,剛開始每個元
    素的級別是1,表明以該元素為根的樹深為1。
*/

/*
    本程式例子是,包含6個結點,分別是1,2,3,4,5,6。7其中1,2,
    3,7屬於一個集合,4,5屬於一個集合。6屬於一個集合
*/

#include <iostream>
#include <cstring>
#include<algorithm>
using namespace std;

const int maxn = 10000;
int par[maxn];     //結點的父母
int r[maxn];    //每個結點的等級,即以該結點為根的樹的深度

//初始化n個元素,父母是他自己,等級(樹深)初始化為1
void init(int n)
{
    for (int i = 1; i <=n; i++) {
        par[i] = i;
        r[i] = 1;
    }
}

//查詢樹根
int find_root(int x) {
    if (par[x] == x) {
        return x;
    }
    else {
        return find_root(par[x]);
    }
}

//合併x和y所屬集合
void united(int x, int y) {
    x = find_root(x);
    y = find_root(y);
    if (x == y) return;//屬於同一棵樹,無需進行合併
//深度淺的樹合併到深度大的樹上
    if (r[x]<r[y]) {
        par[x] = y;r[y]=max(r[x]+1,r[y]);
    } else {
        par[y] = x;r[x]=max(r[x],r[y]+1);
    }
}

//判斷x和y是否屬於同一個集合
bool same_set(int x, int y) {
    return find_root(x) == find_root(y);
}

int main()
{
    int n,m;
    cout<<"請輸入元素的總數: ";cin>>n;init(n);
    cout<<"請輸入集合的總數: ";cin>>m;
    cout<<"輸入每個集合中的任意兩個元素(最多情況數為該集合元素數減1):"<<endl;
    //n個元素構成m個集合,最多隻需輸入n-m對元素即可完成所有關係建立
    //比如1,2,3,7為一個集合,4,5為一個集合,6為一個集合,7個元素
    //形成3個集合,只需要輸入1、2,1、3,4、5這三對(<=7-3)即可
    int e,p;
    for(int i=1;i<=n-m;i++){
        cin>>e>>p;
        p=find_root(p);
        par[e]=p;   //路徑壓縮,e的父母直接變為p的樹根
        r[p]=2; //由於建立的樹只有兩層,孩子層深度為1,父母層深度為2
        //這樣建立起來的樹只有兩層,一個父母,眾多個孩子
    }

    cout << "輸入兩個元素,把這兩個元素所在的集合合併: ";
    int e1,e2;
    cin>>e1>>e2;
    united(e1, e2);//把e1,e2所在的兩個集合合併

    cout << "輸入兩個元素,查詢是否屬於一個集合: ";
    cin >> e1 >> e2;
    if(same_set(e1,e2)) cout<<"same"<<endl;
    else cout<<"different"<<endl;

    return 0;
}


/*
    本程式樣例輸入輸出:

請輸入元素的總數: 7
請輸入集合的總數: 3
輸入每個集合中的任意兩個元素(最多情況數為該集合元素數減1):
1 2
2 3
4 5
6 6
輸入兩個元素,把這兩個元素所在的集合合併: 2 5
輸入兩個元素,查詢是否屬於一個集合: 1 4
same

*/

構造一個只含n個頂點,而邊集為空的子圖,若將該子圖中各個頂點看成是各棵樹的根節點,則它是一個含有n棵樹的森林 。之後,從網的邊集中選取一條權值最小的邊,若該邊的兩個頂點分屬不同的樹 ,則將其加入子圖,也就是這兩個頂點分別所在的 兩棵樹合成一棵樹;反之,若該邊的兩個頂點已落在同一棵樹上,則不可取,而應該取下一條權值最小的邊再試之。依次類推,直至森林只有一棵樹。kruskal演算法能夠在並查集的基礎很快的實現。

以下面的圖為例,實現kruskal演算法
這裡寫圖片描述

#include<iostream>
#include<algorithm>
using namespace std;

const int maxn=10000;
int par[maxn];//結點的父母,用並查集
int r[maxn];//每個結點等級(以該結點為樹根的樹深),用並查集

typedef struct{
    int st;//一條邊的起始結點
    int en;//一條邊的結束結點
    int w;//邊權重
}edge_node;

edge_node edge[maxn/2*maxn];

//並查集演算法的初始化
void init(int n){
    for(int i=1;i<=n;i++){
        par[i]=i;
        r[i]=1;
    }
}
//並查集演算法的找樹根
int find_root(int x){
    if(x==par[x]) return x;
    else return find_root(par[x]);
}
//並查集演算法的合併集合
void united(int x,int y){
    x=find_root(x);
    y=find_root(y);
    if(x==y) return;
    if(r[x]<r[y]) {par[x]=y;r[y]=r[x]+1;}
    else{par[y]=x;r[x]=r[y]+1;}
}

bool cmp(edge_node e1,edge_node e2){
    return e1.w<e2.w;
}

int kruskal(int n,edge_node edge[]){
    int sum=0;
    init(n);
    sort(edge+1,edge+n+1,cmp);
    for(int i=1;i<=n;i++){
        if(find_root(edge[i].st)!=(find_root(edge[i].en))){
            united(edge[i].st,edge[i].en);
            sum+=edge[i].w;
        }
    }
    return sum;
}

int main(){
    int n;
    cout<<"請輸入邊的條數: ";cin>>n;
    int x,y,w;
    for(int i=1;i<=n;i++){
        cout<<"請輸入第"<<i<<"條邊的兩個頂點及權重: "<<endl;
        cin>>x>>y>>w;
        edge[i].st=x;edge[i].en=y;edge[i].w=w;
    }
    cout<<"最小生成樹的邊權之和: ";
    cout<<kruskal(n,edge)<<endl;
    return 0;
}

/*
輸入輸出樣例:

請輸入邊的條數: 5
請輸入第1條邊的兩個頂點及權重:
1 2 6
請輸入第2條邊的兩個頂點及權重:
1 3 1
請輸入第3條邊的兩個頂點及權重:
2 3 5
請輸入第4條邊的兩個頂點及權重:
2 4 3
請輸入第5條邊的兩個頂點及權重:
3 4 4
最小生成樹的邊權之和: 8
*/