最小生成樹 Prim算法 和 Kruskal算法,c++描述


以下兩種算法,結合給出的圖的例子,完整實現代碼地址:https://github.com/meihao1203/learning/tree/master/06292018/Graph_Prim_Kruskal
把構造連通網的最小代價生成樹稱為最小生成樹(Minimum Cost Spanning Tree)

普里姆(Prim)算法:
  假設 N = (P,{E})是連通網,TE是N上最小生成樹中邊的集合。算法從U = {u0}(u0∈V),TE = {}開始,(U0是隨便選取的一個頂點)重復執行下述操作:在所有ui ∈U,vi ∈V-U中找一條代價最小的邊(ui,vi) ∈ E 並入集合TE,同時vi並入U,直到U=V為止。此時TE中必有n-1條邊,則T=(V,{TE})為N的最小生成樹。

//圖(2,1)錯了,正確的是18
數組下標 0 1 2 3 4 5 6 7 8
vertex 0 0 0 0 0 0 0 0 0
weight 0 10 11
文件中讀數組的時候,讀到-1就填入int類型能表示的最大值
算法中用到的兩個數組,先指定一個初始頂點0,,初始化后就是上面的樣子,(0,0)=0,(1,0)=10,(2,0)=∞...
其中,weight=0表示該點已經在最小生成樹中,初始選取第0個結點V0

執行完第一趟遍歷,找到了一個最小邊(0,1)=10,把1加入到生成樹里面,1能得到一些新的邊,此時就要更新vertex和weight


數組下標 0 1 2 3 4 5 6 7 8
vertex 0 0 1 0 0 0 1 0 1
weight 0 0 18 11 16 12
(1,2)=18,(1,6)=16,(1,8)=12
/* Prim.cpp */
#include"Prim.h"
#include<iostream>
namespace meihao
{
        void MiniSpanTree_Prim(const meihao::Graph& g)
        {
                //先獲取圖的頂點數用來建立相應的存儲結構
                int vertexNum = g.getGraphVertexNumber();
                int* vertex = new int[vertexNum]();  //初始化一個數組來存儲最終的最小生成樹的結點信息
                //數組下標表示vi,對應的數組值表示vj  vertex[vi] = vj,動態申請空間時初始化,最初vertex都是0
                weight_vaule_type* weight = new weight_vaule_type[vertexNum]();
                //weight數組用來存放邊的權值,在算法運行過程中要用來比較
                //weight中值為0,表示對應的下標表示的點已經在最小生成樹中
                //vi對應的weight[vi]就表示(vi,ji) = weight[vi],其中vj = vertex[vi]

                //1、隨便選取一個點開始求解最小生成樹
                vertex[0] = 0; //(0,0)=0,選取從0號結點開始
                //2、從選取的第0個點開始初始化weight數組,相當於用鄰接矩陣的第一行來初始化weight
                for(int idx=0;idx!=vertexNum;++idx)
                {
                        weight[idx] = g.getGraphEdgeWeight(0,idx);
                } //初始vertex都是0,表示0到其他節點,剛好對應初始化后的weight
                //weight[0]=0,vertex[0]=0,表示(0,0)=0->(vertex[0],0)=weight[0]
                //3、weight數組存放了從0頂點到其他頂點的距離,開始選一個權值最小邊(v0,vj),同時把頂點vj加入vertex中 vertex[vj] = v0; weight[vj] = 0;
                for(int idx=0;idx!=vertexNum;++idx)
                {
                        weight_vaule_type min = max_weight_value;
                        int newVertex = 0;  //定義一個變量保存在一次遍歷過程中找到的最小權值邊的,初始值為0
                        for(int iidx=0;iidx!=vertexNum;++iidx)
                        {
                                if(0!=weight[iidx]&&
                                        weight[iidx]<min)  //weight[idx]=0,表示結點idx已經在我們最終要求的最小生成樹中了
                                {
                                        //找到一條權值相對min小的邊
                                        min = weight[iidx];    //更新min
                                        newVertex = iidx;  //記錄結點,目前(0,iidx)邊的權值最小
                                }
                        }
                        //輸出邊
                        //if(0!=newVertex)  //vertex[0]=0,存放的是最開始初始化的,(0,0)指向自身,不在最小生成樹中
                                cout<<"("<<vertex[newVertex]<<","<<newVertex<<")"<<" ";
                        //把一次遍歷找到的newVertex加入到最小生成樹中
                        weight[newVertex] = 0;  
                        //這時候生成樹多了一個結點,通過這個頂點又可以通過依附在這個點的邊到達其他結點,所以這個時候要更新weight
                        for(int iiidx=0;iiidx!=vertexNum;++iiidx)
                        {
                                if(0!=weight[iiidx]&&
                                        g.getGraphEdgeWeight(newVertex,iiidx)<weight[iiidx])
                                {
                                        weight[iiidx] = g.getGraphEdgeWeight(newVertex,iiidx);
                                        //weight更新了,vertex存放對應的兩個頂點信息,所以這里要同步更新
                                        vertex[iiidx] = newVertex;    //(iiidx,newVertex) = weight[iiidx];
                                }
                        }
                } //每次都能找出一個點,最終找到n個點,n-1條邊,
        }
};
/* Prim.cpp */   根據上面的表,優化左邊的算法,看起來邏輯更清晰
#include"Prim.h"
#include<iostream>
namespace meihao
{
        void MiniSpanTree_Prim(const meihao::Graph& g)
        {
                int vertexNum = g.getGraphVertexNumber();
                int* vertex = new int[vertexNum];  //這里可以直接寫()全部初始化
                int* weight = new int[vertexNum];
                vertex[0] = 0;
                weight[0] = 0;
                for(int idx=1;idx!=vertexNum;++idx)
                {
                        vertex[idx] = 0;
                }
                for(int idx=1;idx!=vertexNum;++idx)
                {
                        weight[idx] = g.getGraphEdgeWeight(0,idx);
                }
                for(int idx=1;idx!=vertexNum;++idx)
                {
                        weight_vaule_type min = max_weight_value;
                        int newVertex;
                        for(int iidx=1;iidx!=vertexNum;++iidx)
                        {
                                if(0!=weight[iidx]&&weight[iidx]<min)
                                {
                                        min = weight[iidx];
                                        newVertex = iidx;   //相當於數組下標
                                }
                        }
                        //一趟遍歷找到一條最小權值的邊
                        cout<<"("<<vertex[newVertex]<<","<<newVertex<<")"<<" ";
                        //newVertex加入生成樹,也就是修改weight
                        weight[newVertex] = 0;
                        //更新vertex和weight數組
                        for(int iiidx=1;iiidx!=vertexNum;++iiidx)
                        {
                                if(0!=weight[iiidx]&&g.getGraphEdgeWeight(newVertex,iiidx)<weight[iiidx])
                                {
                                        weight[iiidx] = g.getGraphEdgeWeight(iiidx,newVertex);
                                        vertex[iiidx] = newVertex;
                                }
                        }
                }
        }
};

/* data.txt */從文件中讀取數據初始化圖的時候,如果是-1就用最大值替代
9
0 1 2 3 4 5 6 7 8
0 10 -1 -1 -1 11 -1 -1 -1
10 0 18 -1 -1 -1 16 -1 12
-1 -1 0 22 -1 -1 -1 -1 8
-1 -1 22 0 20 -1 -1 16 21
-1 -1 -1 20 0 26 -1 7 -1
11 -1 -1 -1 26 0 17 -1 -1
-1 16 -1 -1 -1 17 0 19 -1
-1 -1 -1 16 7 -1 19 0 -1
-1 12 8 21 -1 -1 -1 -1 0 

/* testMain.txt */
#include"Graph.h"
#include"Prim.h"
#include"Kruskal.h"
#include<iostream>
using namespace std;
int main()
{
        meihao::Graph g("data.txt");
        cout<<"MiniSpanTree_Prim:"<<endl;
        meihao::MiniSpanTree_Prim(g);
        cout<<endl;
        system("pause");
}







利用結構體實現->->
#include"Prim.cpp"
#include<iostream>
namespace meihao
{
        typedef struct Arr
        {
                int vi;  //頂點vi
                weight_vaule_type weight;  //(vi,vj)的權值
        }node,*pNode;
        //思路:
        //從結點0開始,定義n-1個node的數組,分別賦值(0,1),(0,2)...
        void MiniSpanTree_Prim(const meihao::Graph& g)
        {
                //獲取頂點個數
                int vertexNum = g.getGraphVertexNumber();
                node* arr = new node[vertexNum]();
                for(int idx=1;idx!=vertexNum;++idx)
                {
                        arr[idx].vi = 0;  //選取的初始結點0
                        arr[idx].weight = g.getGraphEdgeWeight(0,idx);
                }
                for(int idx=1;idx!=vertexNum;++idx)
                {
                        weight_vaule_type min = max_weight_value;
                        int newVertex;
                        for(int iidx=1;iidx!=vertexNum;++iidx)
                        {
                                if(0!=arr[iidx].weight&&arr[iidx].weight<min)
                                {
                                        min = arr[iidx].weight;
                                        newVertex = iidx;
                                }
                        }
                        cout<<"("<<arr[newVertex].vi<<","<<newVertex<<")"<<" ";
                        arr[newVertex].weight = 0;
                        //更新數組
                        for(int iiidx=1;iiidx!=vertexNum;++iiidx)
                        {
                                if(0!=arr[iiidx].weight&&g.getGraphEdgeWeight(newVertex,iiidx)<arr[iiidx].weight)
                                {
                                        arr[iiidx].vi = newVertex;
                                        arr[iiidx].weight = g.getGraphEdgeWeight(newVertex,iiidx);
                                }
                        }
                }
        }
};


克魯斯卡爾(Kruskal)算法:

算法每次都選一個最小權值的邊加入的生成樹的集合中,在加入之前要判斷是否會形成回路;所以算法先存儲所有的邊集數組,對其進行排序,如右圖所示;最后每次選一個最小邊加入最小生成樹。
判斷加入一條邊是否會構成回路,就要利用一個數組(parent)來存放每個結點的父結點。初始全部為0

下標 0 1 2 3 4 5 6 7 8
parent 0 0 0 0 0 0 0 0 0
其中,parent[i] = j,表示i結點的父結點為j結點

加入第一條邊(4,7)=7,parent[4]=0; parent[7]=0,4和7都是單獨的一個根結點,加入邊不會形成回路,默認一個加入規則,parent[4]=7,此時最小生成樹有一條邊,4→7,4的父結點是7。
下標 0 1 2 3 4 5 6 7 8
parent 0 0 0 0 7 0 0 0 0
加入第二條邊(2,8)=8,同上  2→8  4→7
下標 0 1 2 3 4 5 6 7 8
parent 0 0 8 0 7 0 0 0 0
加入第三條邊(0,1)=10, 0→1 2→8 4→7
下標 0 1 2 3 4 5 6 7 8
parent 1 0 8 0 7 0 0 0 0
加入第四條邊(0,5)=11,這里parent[0]=1,0的父結點1,parent[1]=0,1的父結點0;parent[5]=0,所以最后parent[1]=5。這是后的生成樹0→1→5 2→8 4→7
下標 0 1 2 3 4 5 6 7 8
parent 1 5 8 0 7 0 0 0 0

加入第五條邊(1,,8)=12,parent[1]=5,parent[5]=0; parent[8]=0;
0→1→5→8 2→8 4→7   從上面的圖可以看出,現在有兩個頂點結合出現{0,1,5,8,2}和{4,7}
下標 0 1 2 3 4 5 6 7 8
parent 1 5 8 0 7 8 0 0 0
加入第六條邊(3,7)=16,parent[3]=0;parent[7]=0;
0→1→5→8 2→8 4→7 3→7
下標 0 1 2 3 4 5 6 7 8
parent 1 5 8 7 7 8 0 0 0
加入第七條邊(1,6)=16,parent[1]=5,parent[5]=8,parent[8]=0; parent[6]=0;
0→1→5→8→6 2→8 4→7 3→7
下標 0 1 2 3 4 5 6 7 8
parent 1 5 8 7 7 8 0 0 6
加入第八條邊(5,6)=17,parent[5]=8,parent[8]=6,parent[6]=0; parent[6]=0;同一個頂點,不能加入6←6指向自身了
0→1→5→8 2→8 4→7 3→7  ,如果加入邊(5,6),5-8-6-6,這就是一個環了
下標 0 1 2 3 4 5 6 7 8
parent 1 5 8 7 7 8 0 0 6
加入第九條邊(1,2)=18,parent[1]=5,parent[5]=8,parent[8]=6,parent[6]=0; parent[2]=8,parent[8]=6,parent[6]=0;
0→1→5→8 2→8 4→7 3→7
下標 0 1 2 3 4 5 6 7 8
parent 1 5 8 7 7 8 0 0 6
加入第十條邊(6,7)=19,parent[6]=0; parent[7]=0;
0→1→5→8 2→8 4→7 3→7 6→7

下標 0 1 2 3 4 5 6 7 8
parent 1 5 8 7 7 8 7 0 6

parent[7] = 0,表示沒有父結點,樹中只要一個結點沒有雙親結點
加入第10條邊之后,此時黃色部分已經有8個,9個頂點的生成樹只能有8條表,此后再加入新的邊,都會構成回路

這種查找一直用到了並查集的思想。初始時把每個對象看作是一個單元素集合;然后依次按順序讀入聯通邊,將連通邊中的兩個元素合並,即找到父結點。

優化1、
其實可以壓縮搜尋路徑,比如0→1→5→8,可以變成0→8,
1→8,58,這樣如果結點個數多的時候,效率就提升。

還有另外一種做法,完全按照並查集的搜索合並來解決,我覺得合並有點多余,我這里的優化1就是借鑒了他的https://www.cnblogs.com/yoke/p/6697013.html
他的最終目的生成樹從(4,7)開始,最后會是4→7,4→2,4→8,... 反正就一個根結點,下面全是葉子結點。這個合並操作也多余了,不會提高效率。

   最終結果
/* Kruskal.cpp */
#include"Kruskal.h"
#include<iostream>
#include<vector>
#include<algorithm>
namespace meihao
{
        bool cmp(const edges& a,const edges& b)
        {
                return a.weight < b.weight;  //從小到大排序
        }
        void readEgdesFromGraph(const meihao::Graph& g,vector<edges>& edgeArr)
        {//無向圖鄰接矩陣都是對稱的,只讀取上三角即可
                int vertexCnt = g.getGraphVertexNumber();
                for(int idx=0;idx!=vertexCnt;++idx)
                {
                        for(int iidx=idx;iidx!=vertexCnt;++iidx)
                        {
                                if(0!=g.getGraphEdgeWeight(idx,iidx)&&max_weight_value!=g.getGraphEdgeWeight(idx,iidx))
                                {
                                        edges tmp;
                                        tmp.begin = idx;
                                        tmp.end = iidx;
                                        tmp.weight = g.getGraphEdgeWeight(idx,iidx);
                                        edgeArr.push_back(::move(tmp));
                                }
                        }
                }
                sort(edgeArr.begin(),edgeArr.end(),cmp);  //從小到大排序
        }
        void MiniSpanTree_Kruskal(const meihao::Graph& g)
        {
                vector<edges> edgeArr;  //邊集數組
                readEgdesFromGraph(g,edgeArr);
                //定義parent數組,數組下標對應唯一的圖結點,數組值對應小標結點的父結點,最小生成樹就是一棵樹
                int vertexCnt = g.getGraphVertexNumber();
                int* parent = new int[vertexCnt]();  //初始化全部為0,parent[i] = 0,表示i結點沒有父結點(只有一個根結點的樹)
                int edgeCnt = edgeArr.size();  //邊集數組大小,也就是圖中邊的數量
                for(int idx=0;idx!=edgeCnt;++idx)
                {
                        int firstFather = find(parent,edgeArr[idx].begin);
                        int secondFather = find(parent,edgeArr[idx].end);
                        if(firstFather!=secondFather)  //待加入的這條邊的父結點相同,如果再把這條邊加入,就會出現環。這里只能不等
                        {//這個過程就是一個找爹過程,最小生成樹只能有一個根結點,如果待加入邊對其兩端的頂點去找爹找到相同的,這時候再加入這條邊就出現環,∧->△
                                parent[firstFather] = secondFather;   //加入該條邊,(firstFather,secondFather),firstFather的父結點secondFather
                                //輸出找到的邊
                                cout<<"("<<edgeArr[idx].begin<<","<<edgeArr[idx].end<<")"<<" ";
                        }
                }
                cout<<endl;
        }
//沒有優化的find
        //int find(int* parent,int vi)
        //{
        //        while(parent[vi]>0)  //vi結點有父結點
        //        {
        //                vi = parent[vi];
        //        }
        //        return vi;
        //}
      
};
  int find(int* parent,int vi)
        {// 優化1、
                int viTmp = vi;
                while(parent[vi]>0)  //vi結點有父結點
                {
                        vi = parent[vi];  //找父結點
                }
                while(vi!=viTmp)
                {//vi有父結點,遍歷,如果有父結點還有祖先結點,假設eg:0→1→5(0的父結1,1的父親5) 變成 0→5,1→5
                        int tmp = parent[viTmp];  //暫存最初vi結點(0)的父結點(tmp=1)
                        parent[viTmp] = vi;  //(parent[0]=5)
                        viTmp = tmp;  //0變成1;
                }
                return vi;
        }


/* Kruskal.h */
#ifndef __KRUSCAL_H__
#define __KRUSCAL_H__
#include"Graph.h"
namespace meihao
{
        typedef struct EdgeSetArr  //邊集數組
        {
                int begin;  //邊起點
                int end;    //邊終點
                weight_vaule_type weight;  //邊權值
        }edges;
        void readEgdesFromGraph(const meihao::Graph& g);  //從圖中讀出我們需要的邊集數組
        int find(int* parent,int vi);
        void MiniSpanTree_Kruskal(const meihao::Graph& g);
};
#endif





/* maintext.cpp */
#include"Graph.h"
#include"Prim.h"
#include"Kruskal.h"
#include<iostream>
using namespace std;
int main()
{
        meihao::Graph g("data.txt");
        cout<<"MiniSpanTree_Prim:"<<endl;
        meihao::MiniSpanTree_Prim(g);
        cout<<endl<<endl;
        cout<<"MiniSpanTree_Kruskal"<<endl;
        meihao::MiniSpanTree_Kruskal(g);
        cout<<endl;
        system("pause");
}
邊數較少可以用Kruskal,因為Kruskal算法每次查找權值最小的邊。 邊數較多可以用Prim,因為它是每次加一個頂點,對邊數多的適用。




免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM