以下兩種算法,結合給出的圖的例子,完整實現代碼地址: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,,初始化后就是上面的樣子,(0,0)=0,(1,0)=10,(2,0)=∞... 其中,weight=0表示該點已經在最小生成樹中,初始選取第0個結點V0 執行完第一趟遍歷,找到了一個最小邊(0,1)=10,把1加入到生成樹里面,1能得到一些新的邊,此時就要更新vertex和weight
(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
①加入第一條邊(4,7)=7,parent[4]=0; parent[7]=0,4和7都是單獨的一個根結點,加入邊不會形成回路,默認一個加入規則,parent[4]=7,此時最小生成樹有一條邊,4→7,4的父結點是7。
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
⑤加入第五條邊(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}
|
⑥加入第六條邊(3,7)=16,parent[3]=0;parent[7]=0; 0→1→5→8 2→8 4→7 3→7
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
⑦加入第七條邊(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
|
⑧加入第八條邊(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,這就是一個環了
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
⑨加入第九條邊(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
|
⑩加入第十條邊(6,7)=19,parent[6]=0; parent[7]=0; 0→1→5→8 2→8 4→7 3→7 6→7
parent[7] = 0,表示沒有父結點,樹中只要一個結點沒有雙親結點 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
加入第10條邊之后,此時黃色部分已經有8個,9個頂點的生成樹只能有8條表,此后再加入新的邊,都會構成回路 這種查找一直用到了並查集的思想。初始時把每個對象看作是一個單元素集合;然后依次按順序讀入聯通邊,將連通邊中的兩個元素合並,即找到父結點。 優化1、 其實可以壓縮搜尋路徑,比如0→1→5→8,可以變成0→8,1→8,5→8,這樣如果結點個數多的時候,效率就提升。 還有另外一種做法,完全按照並查集的搜索合並來解決,我覺得合並有點多余,我這里的優化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");
}
|