正在加载……
专注、离线、切勿分心
以下两种算法,结合给出的图的例子,完整实现代码地址: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,因为它是每次加一个顶点,对边数多的适用。



posted on 2018-06-29 14:01  正在加载……  阅读(1276)  评论(0编辑  收藏  举报