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

以前学图论的时候感觉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 <iostream>
#include <string.h> 
#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 <iostream>
#include <algorithm>
#include <vector>
#define N 9
using namespace std;
class Node{
	public://class默认private 
	    int start;
	    int end;
		int val;
};
vector<Node> 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<y.val;});//按边的长度进行升序排序,第三个参数为匿名函数,上篇博客有稍作介绍 
    cout << Kruskal(0)<<endl;
    return 0;
}

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<y.val;});
bool cmp(Node x,Node y){//在主函数前声明一下
    return x.val<y.val;
}

sort(V.begin(),V.end(),cmp);//将这行代替上面的sort就好

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面我会分别介绍普里姆算法克鲁斯卡尔算法,并给出它们的代码实现。 ### 普里姆算法 普里姆算法是一种贪心算法,用于求解加权无向连通图的最小生成树。该算法从任意一个顶点开始,每次选择一条权值最小的边,将其加入到生成树中,直到所有顶点都被加入到生成树中为止。 普里姆算法的时间复杂度为 $O(ElogV)$,其中 $V$ 表示顶点数,$E$ 表示边数。 下面是普里姆算法的 Python 代码实现: ```python import heapq def prim(graph, start): mst = [] # 用于存储最小生成树的边 visited = set([start]) # 记录已经访问过的节点 candidates = [(weight, start, end) for end, weight in graph[start].items()] heapq.heapify(candidates) # 将初始的候选边加入小根堆中 while candidates: weight, start, end = heapq.heappop(candidates) if end not in visited: # 如果当前边的终点没有被访问过 visited.add(end) mst.append((start, end, weight)) for next_end, weight in graph[end].items(): if next_end not in visited: heapq.heappush(candidates, (weight, end, next_end)) # 将新的候选边加入小根堆中 return mst ``` 其中,`graph` 是一个字典,表示图的邻接表形式,`start` 是起始节点的编号。 ### 克鲁斯卡尔算法 克鲁斯卡尔算法也是一种贪心算法,用于求解加权无向连通图的最小生成树。该算法的基本思想是,将所有边按照权值从小到大排序,依次取出每条边,如果这条边的两个端点不在同一个连通块中,就将它们合并,直到所有节点都在同一个连通块中为止。 克鲁斯卡尔算法的时间复杂度为 $O(ElogE)$,其中 $E$ 表示边数。 下面是克鲁斯卡尔算法的 Python 代码实现: ```python def kruskal(graph): edges = [(weight, start, end) for start in graph for end, weight in graph[start].items()] edges.sort() # 将所有边按照权值从小到大排序 parent = {node: node for node in graph} # 用于记录每个节点的父节点 mst = [] # 用于存储最小生成树的边 for weight, start, end in edges: while start != parent[start]: # 找到 start 的根节点 start = parent[start] while end != parent[end]: # 找到 end 的根节点 end = parent[end] if start != end: # 如果 start 和 end 不在同一个连通块中 mst.append((start, end, weight)) parent[end] = start # 将 end 的根节点设为 start 的根节点 return mst ``` 其中,`graph` 是一个字典,表示图的邻接表形式。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值