Prim普里姆和Kruskal克鲁斯卡尔算法(最小生成树)

Prim普里姆和Kruskal克鲁斯卡尔算法(最小生成树),第1张

以前学图论的时候感觉prim和kruskal理论都理解了,prim不就是基于点,kruskal不就是基于边嘛,自以为掌握了,但某一天发现需要自己写的时候却无从下手,所以还是决定把这两个算法写一遍。

最小生成树,就是以最小的代价将所有的点连通起来,打个比方就是将各个独立的村落修道路将所有村落连接起来,待会看图就可理解了。

关于这两算法的详细介绍可参考图的基本算法解析(最小生成树) - 综合编程类其他综合 - 红黑联盟,本文图是他的,代码也是参考他的依照自己的理解进行了一些小优化,下文会标出。

1、Prim

Prim我个人的理解就是集合开始往外扩张。也就是部落从一个小村庄开始,他们想最有效的征服周围的村落,那么就把目标定为离该部落最近的村落,收复一个村落后把该点也加进来考虑下一个最近的村落,直至全部村落都在部落内(理解为修铁路也可以,将所有村落连起来)。

(1)选择起点

(2)从可移动的路径中找到最短路径

(3)判断此最短路径的两点是否已经在集合中,若在则删除此路径,不在则加入集合

(4)重复(2)(3)步骤直到所有点都已经在集合内,得出总路径

假设从a点开始,目前可移动的路径有7,4,那么选4,连接ac;

现在集合内拥有的点有a和c,可移动路径有7,6,8,9,选最小值6,连接cb;

重复执行,最后边的顺序为4,6,2,4,1,点的顺序为acbdfe;

#include 
#include  
#define INF 10000
using namespace std;
const int N = 6;//6个点嘛 
bool visit[N];//此点是否已在集合内 
int dist[N] = {0};//当前集合离各个点的最小距离 
int graph[N][N] = { {INF,7,4,INF,INF,INF},  //邻接矩阵,INF代表两点之间不可达
                    {7,INF,6,2,INF,4},//一维数组代表点,二维代表该点到其他点的距离 
                    {4,6,INF,INF,9,8},//对角线为自身到自身,所以都是INF 
                    {INF,2,INF,INF,INF,7},
                    {INF,INF,9,INF,INF,1},
                    {INF,4,8,7,1,INF}
                  };
int prim(int cur)
{
    int index = cur;//起始点 
    int sum = 0;//总路径 
    int i = 0;
    int j = 0;
    cout << index << " ";//debug输出起始点 
    memset(visit,false, sizeof(visit));//所有点未在集合内 
    visit[cur] = true;//起点加入集合 
    for(i = 0; i < N; i++)
        dist[i] = graph[cur][i];//初始化,每个起点邻接的点的距离存入dist,这里存入 {INF,7,4,INF,INF,INF}
    for(i = 1; i < N; i++)//因为起点已经初始化过,故少遍历一次,i=1,而非i=0 
    {
        int minor = INF;//dist内的最小值 
        for(j = 0; j < N; j++)
        {
            if(!visit[j] && dist[j] < minor)//找到未访问的点中,距离当前最小生成树距离最小的点
            {
                minor = dist[j];//更改最小值 
                index = j;//最小边所连接的点 
            }
        }
        visit[index] = true;//该点加入集合 
        cout << index << " ";//debug输出,该点 
        sum += minor;//总路程更新 
        for(j = 0; j < N; j++)
        {
            if(!visit[j] && dist[j]>graph[index][j])//执行更新,更新集合内到每个点的最小路径,对比一下新加入的点是否有更近路径,只判断集合外的点 
            {
                dist[j] = graph[index][j];
            }
        }
    }
    cout << endl;//换行 
    return sum;//返回最小生成树的总路径值
}
int main()
{
    cout << prim(0) << endl;//从顶点a开始
    return 0;
}

graph[N][N]二维邻接矩阵代表点(一维)和点到每个点的距离(二维),

visit[N]代表是否在集合内,

dist[N]代表当前集合到各个点的最小距离,

总体流程为:

(1)初始化,将起点加入集合,visit=true,并获取其各个路径加入dist

(2)遍历N-1次(起点已经单独判断过,所以次数-1),在dist找到最短路径(只判断集合外的点,即!visit[j]),将最短的路径所连接的点加入集合,即visit=true。

(3)加入了新点后,获取该点到其他点的距离(只判断集合外的点,即!visit[j]),若有到其他点的更短路径,更新dist。

(4)重复(2)(3)直至全部点加入集合。

注释应该写的比较详细,结合代码和注释应该不难看懂。

2、Kruskal

Prim理解为集合从一个点开始慢慢吸纳其它的点进来。Kruskal则是以边为出发点,最短的路径先连起来,连接成好几个小集合再合并。举上文的例子就是,各个点分别为一个部落,其中距离相近的两个部落先进行合并,合并成了几个中等规模部落,然后这几个部落再按照距离近的进行合并。怪怪的,看下流程吧:

(1)每个点作为一个集合,将边的距离进行升序排序

(2)找到最短的路径,判断该边的两顶点是否在同一集合内,若在则跳过,将该边踢出检测队列,若不在则合并两集合

(3)重复(2)直到所有顶点都在同一集合内

注意图跟我放的链接文章不太一样,仍然为Prim的图,9条边。

第一步找到最短的边,长度为1,连接ef;

第二步找到最短的边,长度为2,连接bd;

第三步找到最短的边,长度为4,连接ac(bf也可,看排序细节了);

第四步找到最短的边,长度为4,连接bf;

注意:此时若de存在长度为5的边不会连接,因为在同一集合内,已经连同了!!!

第五步找到最短的边,长度为6,连接bc;

#include 
#include 
#include 
#define N 9
using namespace std;
class Node{
	public://class默认private 
	    int start;
	    int end;
		int val;
};
vector V(N);//边 

int edge[N][3] = {  {0,1,7},//start起点,end终点,val路径长度 
                    {0,2,4}, 
                    {1,2,6}, 
                    {1,3,2},
                    {1,5,4},
                    {2,4,8}, 
                    {2,5,9}, 
                    {3,5,7}, 
                    {4,5,1}
                	};
 
int father[N] = {0};//属于哪个集合,比如father[4]=0,4节点是属于0这个集合内的 
int cap[N] = {0};//可以理解为集合优先级,也可以理解为吞并其它集合的次数,并非集合的元素个数,详细见下文 
 
void make_set()//初始化集合,让所有的点都各成一个集合,每个集合都只包含自己
{
    for(int i = 0; i < N; i++)
    {
        father[i] = i;//每个点各自为一个集合 
        cap[i] = 1;//优先级都为1 
    }
}
 
int find_set(int x)//判断一个点属于哪个集合,点如果都有着共同的祖先结点,就可以说他们属于一个集合
{
    if(x != father[x])//如果点不在原始的集合,即已经加入了其它集合 
    {                             
        father[x] = find_set(father[x]);//获取该点所在的上级集合,上级若也并非原始集合,则递归找到最终的集合 
    }    
    return father[x];
}                                 
 
void Union(int x, int y)//将x,y合并到同一个集合
{
    x = find_set(x);//找到最终集合 
    y = find_set(y);
    if(x == y)//同一个集合则返回,无需合并 
        return;
    if(cap[x] < cap[y])//优先级比他小,则归入他的集合 
        father[x] = y;//原来是find_set(y),但直接写y也一样 
    else
    {
        if(cap[x] == cap[y])//如果优先级相等 
            cap[x]++;//那么X的优先级增加,也可以理解成x吞并其它集合的次数,cap[x]不能理解为集合内元素的数量,不然此处应该为cap[x]+=cap[y]; 
        father[y] = x;//y归入x ,x原为find_set(x) 
    }
}
 
int Kruskal(int n)
{
    int sum = 0;
    make_set();
    for(int i = 0; i < N; i++)//将边的顺序按从小到大取出来
    {
        if(find_set(V[i].start) != find_set(V[i].end))//如果边的两个顶点不在一个集合中,就并到一个集合里,生成树的长度加上这条边的长度
        {
            Union(V[i].start, V[i].end);  //合并两个顶点到一个集合
            sum += V[i].val;//总路径增加 
        }
    }
    return sum;
}
int main()
{
    for(int i = 0; i < N; i++)//初始化边的数据,在实际应用中可根据具体情况转换并且读取数据,这边只是测试用例
    {
        V[i].start = edge[i][0];
        V[i].end = edge[i][1];
        V[i].val = edge[i][2];
    }
    sort(V.begin(),V.end(),[](Node x,Node y)->bool{return x.val

edge[N][3]:N指的是9条边,3是边的三个属性,左边的顶点和右边的顶点、长度(无向图,并非单向,两点反过来也一样)

father[N]:该点属于哪个集合,需要递归判断,比如a属于b,但b又属于c,也就是a其实是属于c

cap[N] :可以理解为集合优先级,也可以理解为吞并其它集合的次数,并非集合的元素个数

(1)初始化,依照边的长度进行升序排序。各个点分别为自己的集合

(2)排序后遍历每条边,若边两点不在一个集合内,则合并,若在一个集合内则不处理直接跳过

(3)重复执行(2)直到所有点都在同一集合内

注意:如果报错应该是匿名函数执行不了,这部分是C++11的新特性,老版本的编译器可能执行不了,需要在编译器上更改配置,或者直接不用匿名函数,按照下文进行修改即可

//sort(V.begin(),V.end(),[](Node x,Node y)->bool{return x.val

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/1295677.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-10
下一篇 2022-06-10

发表评论

登录后才能评论

评论列表(0条)