无向图的算法:Kruskal算法与Prim算法生成最小生成树

238 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第25天,点击查看活动详情

最小生成树

先来看一个问题: image.png

在上述图中,描述了学校、农场等6个地点,并用权值标志了各个地点之间的道路距离,现在假设我需要用最小的边,去连通图中所有的地点,这个最小边连通的树就是它的最小生成树。

生成树的属性

  • 一个连通图可以有多个生成树;
  • 一个连通图的所有生成树都包含相同的顶点个数和边数;
  • 生成树当中不存在环;
  • 移除生成树中的任意一条边都会导致图的不连通, 生成树的边最少特性;
  • 在生成树中添加一条边会构成环。
  • 对于包含n个顶点的连通图,生成树包含n个顶点和n-1条边;
  • 对于包含n个顶点的无向完全图最多包含nn2n^{n-2}颗生成树。

最小生成树

最小生成树,就是无向图中边的权值最小的生成树

对于一个带权连通无向图G=(V,E),生成树不同,每棵树的权(树中所有边上的权值和)也不同,设R为G的所有生成树的集合,若T为R中权值和最小的生成树,则T称为G的最小生成树(Minimum-Spanning-Tree,MST)

注:

  • 1、最小生成树可能有多个,但边的权值之和总是唯一且最小的
  • 2、最小生成树的边数=定点数-1,砍掉一条则不连通,增加一条则会出现回路
  • 3、若一个连通图本身就是一颗树,则其最小生成树就是它本身
  • 4、只有连通图才有生成树,非连通图只有生成森林

生成最小生成树的算法有两种:Kruskal算法(克鲁斯卡尔)、普里姆算法(Prim)。

Kruskal算法

每次选择 权值最小且不违反最小生成树属性(不构成环) 的边,使这条边的两头连通(原本已近连通则不选)直到所有结点都连通。

思路

  • 1.把所有边进行排序
  • 2.先把最小的边考虑进去,然后再看是否形成环,如果成环了就不要把这个边考虑进去,如果没有,进入下一步
  • 3.再把下一个小的边考虑进去,再判断是否形成环,如果成环了就不要把这个边考虑进去,如果没有,进入下一步
  • 4.直到连通所有

难点

Kruskal算法的难点在于:如何判断添加一条边后是否形成环?

主要是在于实现集合查询与集合合并。首先,每个点都属于一个单独的集合,当把一个边考虑进去的时候,先判断大的集合里有没有包含这个节点,如果里面有这个节点了就表示形成环了,否则就将把这个节点所在的集合合并进去。

代码实现

public class Kruskal {
     // 这样是可以的,但是没有并查集快
    public static class MySets{
        public HashMap<Node, List<Node>> setMap;
        public MySets(List<Node> nodes){
            for(Node cur: nodes){
                List<Node> set = new ArrayList<Node>();
                set.add(cur);
                setMap.put(cur, set);
            }

        }
        public MySets(List<Node> nodes){
            for(Node cur: nodes){
                List<Node> set = new ArrayList<Node>();
                set.add(cur);
                setMap.put(cur, set);
            }

        }
        public MySets(List<Node> nodes){
            for(Node cur: nodes){
                List<Node> set = new ArrayList<Node>();
                set.add(cur);
                setMap.put(cur, set);
            }

        }
    }
    
    // K算法(并查集)
    public static Set<Edge> KruskalMST(Gragh graph){
        UnionFind unionFind = new UnionFind();  // 生成一个并查集结构
        unionFind.makeSets(graph.nodes.values());
        // 利用比较器将边进行排序
        PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());
        for(Edge edge:graph.edges){  // M条边
            priorityQueue.add(edge);   // O(logM)
        }
        Set<Edge> result = new HashSet<>();
        while(!priorityQueue.isEmpty()){   // M条边
            Edge edge = priorityQueue.poll();    //  O(logM)
            if(!unionFind.isSameSet(edge.form, edge.to)){   // O(1)
                result.add(edge);
                unionFind.union(edge.from, edge.to);
            }
        }
        return result;
    }
}

Prim算法

举个例子

image.png

如图所示,如果我们要对它进行Prim算法操作,那么数据演示如下:

    1. 假设从A点出发(Prim算法对出发的点没有要求,随意选一个出发点即可),此时就释放了三条边:A-B、A-C、A-D,从中选取权值最小的边A-C,那么A、C两点被选中
    1. C点被选中,那么C-B、C-D、C-E、C-F四个边也被释放,从中选取权值最小的边C-F(A点已经被选中了,所以不能选C-A这个边了),此时A、C、F三点被选中
    1. F点被选中,那么F-D、F-E两个边也被释放,从中选取权值最小的边F-D,此时A、C、F、D四点被选中
    1. D点被选中,没有新的边需要被释放,此时在全局范围内(指被释放的但未曾被选中权值最小的边)挑权值最小的边,也就是A-B(6)、A-D(5)、C-B(5)、C-D(5)、C-E(6)、F-E(6)中挑选,其中A-D(5)、C-D(5)涉及的点都曾被选中了,因此只能选C-B(5)这个边,此时A、C、F、D、B五点被选中
    1. B点被选中,B-E边被释放,此时A、C、F、D、B、E已经全部被选中,最小生成树已经形成,A-C、C-F、F-D、C-B、B-E所形成的就是最小生成树

代码实现

public static class EdgeComparator implements Comparator<Edge>{
    public int compare(Edge o1, Edge o2){
        return o1.weight - o2.weight
    }
}
public static Set<Edge> primMST(Graph graph){
    // 解锁的边进入小根堆
    PriorityQueue<Edge> PriorityQueue = new PriorityQueue<>(new EdgeComparator());
    HashSet<Node> set = new HashSet<>();
    
    Set<Edge> result = new HashSet<>();  // 依次挑选的边在result里
    
    for(Node node: graph.nodes.values()){  // 随便挑了一个点
        // node 是开始点
        if(!set.contains(node)){
            set.add(node);
            for(Edge edge: node.edges){   // 由一个点,解锁所有相连的边
                priorityQueue.add(edge);
            }
            while(!priorityQueue.isEmpty()){
                Edge edge = priorityQueue.poll(); // 弹出解锁的边中,最小的边
                Node toNode = edge.to;     // 可能的一个新的点
                if(!set.contains(toNode)){     // 不含有的时候,就是新的点
                    set.add(toNode);
                    result.add(edge);
                    for(Edge nextEdge: toNode.edges){
                        priorityQueue.add(nextEdge);
                    }
                }
            }
        }
    }
    return result;

}

相关Leetcode题

剑指 Offer II 118. 多余的边
684. 冗余连接
1584. 连接所有点的最小费用
685. 冗余连接 II
721. 账户合并
1135. 最低成本联通所有城市
1489. 找到最小生成树里的关键边和伪关键边
305. 岛屿数量 II

相关链接

图解:什么是最小生成树?
最小生成树