以下两种算法,结合给出的图的例子,完整实现代码地址: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");
}
|