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

144 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第15天,点击查看活动详情

背景

一个连通图的生成树是一个极小联通子图,它含有的全部顶点,足以构成n-1条边。(一棵生成树加一条边必然构成一个环)

这句话是我们百度最小生成树最容易看到的,但是关于这句话的深入理解是什么样的呢?

首先:什么是联通图

图从一个顶点到达另一顶点,若存在至少一条路径,则称这两个顶点是连通着的 无向图中,如果任意两个顶点之间都能够连通,则称此无向图为连通图。 (与之对应的是强联通图:有向图中,若任意两个顶点 Vi 和 Vj,满足从 Vi 到 Vj 以及从 Vj 到 Vi 都连通,也就是都含有至少一条通路,则称此有向图为强连通图。)

什么是极小联通子图

“极小”是指边最少的连通子图,去掉任何一个边都会使其变的不连通。

我们知道图包括点和边,那么子图就是缺失一些点和边的图

这里我们需要包括全部的点,所有可以堆构成环的边去掉

image.png

克鲁斯卡尔

克鲁斯卡尔算法查找最小生成树的方法是:将连通网中所有的边按照权值大小做升序排序,从权值最小的边开始选择,只要此边不和已选择的边一起构成环路,就可以选择它组成最小生成树。对于 N 个顶点的连通网,挑选出 N-1 条符合条件的边,这些边组成的生成树就是最小生成树。

例如:

image.png 画出这个图的最小生成子树

  1. 写出a——>b的权值,并排序

image.png

  1. 拿到空图进行连线

image.png image.png image.png image.png image.png image.png

接下来的连线都会出现环,所以在这里结束,结果是:

image.png

看上去是不是很简单

普利姆算法

普里姆算法查找最小生成树的过程,采用了贪心算法的思想。对于包含 N 个顶点的连通网,普里姆算法每次从连通网中找出一个权值最小的边,这样的操作重复 N-1 次,由 N-1 条权值最小的边组成的生成树就是最小生成树。

那么,如何找出 N-1 条权值最小的边呢?普里姆算法的实现思路是:

  1. 将连通网中的所有顶点分为两类(假设为 A 类和 B 类)。初始状态下,所有顶点位于 B 类;
  2. 选择任意一个顶点,将其从 B 类移动到 A 类;
  3. 从 B 类的所有顶点出发,找出一条连接着 A 类中的某个顶点且权值最小的边,将此边连接着的 A 类中的顶点移动到 B 类;
  4. 重复执行第 3  步,直至 B 类中的所有顶点全部移动到 A 类,恰好可以找到 N-1 条边。

看着描述是不是有点蒙,我们接下来实践看看

把图分成两部分,即我们构造的和未构造的,使用一个数组,记录u到u->v的最小代价边。比如从V1开始,遍历所有相关边,然后选择最小一个,从最小那个开始,遍历他相关的边,再次找到最小的,然后从最小的重复过程

比如刚才那个图画生成子图

image.png

代码实现

普利姆算法


算法伪代码:

把所有边排序,记第i小的边为e[i]
初始化MST为空
for(int i=0;i<m;i++){
初始化连通分量,让每个点自成一个独立的连通分量
   if(e[i].u或e[i].v不在同一个连通分量中){
    把e[i]加入MST
    合并e[i].u和e[i].v所在的连通分量
   }
}

在算法实现中,边的排序可直接使用库函数,判断两个点是否在同一个连通分量中,可使用并查集方法。

并查集:把每个连通分量看成一个集合,该集合包含了连通分量中的所有点,每个连通分量用树来表示该集合,每棵树的根节点是这棵树的代表元。

如果两个节点所在树的根节点相同,则代表这两个点在同一个连通分量中。使用路径压缩可加快遍历过程。

class Edge implements Comparable<Edge>{  
    int u;  
    int v;  
    int weight;  
    Edge(int u,int v,int weight){  
        this.u=u;  
        this.v=v;  
        this.weight=weight;  
    }  
    public int compareTo(Edge e) {  
        if(weight>e.weight)  
            return 1;  
        else if(weight<e.weight)  
            return -1;  
        return 0;  
    }  
}  

需要一个数组存储权值。

void toPrim(int w[][], int f[], int n) {
        //用于存放结点的权值的集合
        int d[] = new int[INF];
        int k = 0;
        int m;
        //第一个结点与其它结点的权值加入集合中
        for(int j = 1; j <= n; j++) {
            d[j] = (j == 1 ? 0 : w[1][j]);
            //第一个结点没有父结点,初始化为1
            f[j] = 1;
        }
        //从第二个结点开始
        for(int i = 2; i <= n; i++) {
            m = INF;
            //遍历与当前结点相连接的各个结点的权值并找出具有最小权值的结点
            for(int j = 1; j <= n; j++) {
                if(d[j] <= m && d[j] != 0) {
                    m = d[j];
                    k = j;
                }   
            }
            //将上面找到的结点加入到集合中
            d[k] = 0;
            //更新父d[],将k结点与其它结点连接的最小权值放进d[j]中
            for(int j = 1; j <= n; j++) {
                if(d[j] > w[k][j] && d[j] != 0) {
                    d[j] = w[k][j];
                    f[j] = k;
                }
            }
        }