目录

普里姆算法 (Prim's algorithm)

Minimum Spanning Trees - Prim's Algorithm

普里姆算法(Prim’s algorithm),图论中的一种算法,可在加权连通图里搜索最小生成树。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小。该算法于1930年由捷克数学家沃伊捷赫·亚尔尼克发现;并在1957年由美国计算机科学家罗伯特·普里姆独立发现;1959年,艾兹格·迪科斯彻再次发现了该算法。因此,在某些场合,普里姆算法又被称为DJP算法、亚尔尼克算法或普里姆-亚尔尼克算法。

描述

从单一顶点开始,普里姆算法按照以下步骤逐步扩大树中所含顶点的数目,直到遍及连通图的所有顶点。

  1. 输入:一个加权连通图,其中顶点集合为 $V$,边集合为 $E$;
  2. 初始化:$V_{new}={x}$,其中 x 为集合 $V$ 中的任一节点(起始点),$E_{new}={ }$;
  3. 重复下列操作,直到$V_{new}=V$
    1. 在集合 $E$ 中选取权值最小的边 $(u,v)$,其中 $u$ 为集合 $V_{new}$ 中的元素,而 $v$ 则是 $V$ 中没有加入 $V_{new}$ 的顶点(如果存在有多条满足前述条件即具有相同权值的边,则可任意选取其中之一);
    2. 将 $v$ 加入集合 $V_{new}$ 中,将 $(u,v)$ 加入集合 $E_{new}$ 中;
  4. 输出:使用集合 $V_{new}$ 和 $E_{new}$ 来描述所得到的最小生成树。

时间复杂度

  • 最小边、权的数据结构 - - - 时间复杂度(总计)
  • 邻接矩阵、搜索 - - - $O(|V|^2)$
  • 二叉堆、邻接表 - - - $\displaystyle O((|V|+|E|)\log|V|)=O(|E|\log|V|)$
  • 斐波那契堆、邻接表 - - - ${\displaystyle O(|E|+|V|\log |V|)}$

通过邻接矩阵图表示的简易实现中,找到所有最小权边共需 $O(|V|^{2})$ 的运行时间。使用简单的二叉堆与邻接表来表示的话,普里姆算法的运行时间则可缩减为 ${\displaystyle O(|E|\log |V|)}$,其中 $|E|$ 为连通图的边集大小,$|V|$ 为点集大小。如果使用较为复杂的斐波那契堆,则可将运行时间进一步缩短为 ${\displaystyle O(|E|+|V|\log |V|)}$,这在连通图足够密集时(当 $|E|$ 满足 ${\displaystyle \Omega (|V|\log |V|)}{\displaystyle \Omega (|V|\log |V|)}$ 条件时),可较显著地提高运行速度。

Java 实现

  • 问题一:如何判断一个图是否为原图的生成子图

  • 问题二:如何判断生成子图是一棵树

  • 问题三:如何获得最小生成树

  • 利用inMST[]数组来判断所有节点是否已在生成树中,详细实现可见allConnected()方法

  • 利用inMST[]数组来记录已加入生成树中的节点,以保证无环

  • 利用优先队列按照边的权重从小到大排序,可保证最终的生成子树为最小生成子树

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Graph {
    private int numOfNodes;
    private boolean directed;
    private boolean weighted;
    private double[][] matrix;
    
    // 该数组保存属于 MST 的边的值,这些边将节点连接到其父节点
    private double[] edges;
    // 提供有关每个节点的父节点的信息
    private double[] parents;
    // 正在检查的节点是否已经属于 MST
    private boolean[] includedInMST;
    
    private boolean[][] isSetMatrix;
    
    public Graph(int numOfNodes, boolean directed, boolean weighted) {
    this.directed = directed;
    this.weighted = weighted;
    this.numOfNodes = numOfNodes;

    // Simply initializes our adjacency matrix to the appropriate size
    matrix = new double[numOfNodes][numOfNodes];
    isSetMatrix = new boolean[numOfNodes][numOfNodes];
    
    edges = new double[numOfNodes];
    parents = new double[numOfNodes];
    includedInMST = new boolean[numOfNodes];

    for(int i = 0; i < numOfNodes; i++){
        edges[i] = Double.POSITIVE_INFINITY;
        parents[i] = -1;
        includedInMST[i] = false;
    }
}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class Prim {
    // 存在横切边的数据结构
    private final Queue<int[]> pq;
    // 记录已经成为最小生成树的节点
    private boolean[] inMST;
    // 记录最小生成树的权重和
    private Integer weightSum = 0;
    // graph 是用邻接表表示的一幅图
    // graph[s] 记录节点 s 所有相邻的边
    // 三元组 int[]{from, to, weight} 表示一条边
    private final List<int[]>[] graph;

    public Prim(List<int[]>[] graph) {
        this.graph = graph;
        // 按照边的权重从小到大排序
        this.pq = new PriorityQueue<>(Comparator.comparingInt(a -> a[2]));
        // 图中有 n 个节点
        int n = graph.length;
        this.inMST = new boolean[n];
        // 随便从一个点开始切分都可以,我们不妨从节点 0 开始
        inMST[0] = true;
        cut(0);
        // 不断进行切分,向最小生成树中添加边
        while (!pq.isEmpty()) {
            int[] edge = pq.poll();
            int to = edge[1];
            int weight = edge[2];
            if (inMST[to]) {
                // 节点 to 已经在最小生成树中,跳过
                // 否则这条边会产生环
                continue;
            }
            // 将边 edge 加入最小生成树
            weightSum += weight;
            inMST[to] = true;
            // 节点 to 加入后,进行新一轮切分,会产生更多横切边
            cut(to);
        }
    }
    // 将 s 的横切边加入优先队列
    private void cut(int x) {
        // 遍历 s 的邻边
        for (int[] edge : graph[x]) {
            int to = edge[1];
            if (inMST[to]) {
                // 相邻接点 to 已经在最小生成树中,跳过
                // 否则这条边会产生环
                continue;
            }
            // 加入横切边队列
            pq.offer(edge);
        }
    }
    // 最小生成树的权重和
    public int weightSum() {
        return this.weightSum;
    }
    // 判断最小生成树是否包含图中的所有节点
    public boolean allConnected() {
        for (int i = 0; i < graph.length; i++) {
            if (!inMST[i]) {
                return false;
            }
        }
        return true;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public class Prim {
    public static List<Vertex> vertexList = new ArrayList<Vertex>();//结点集
    public static List<Edge> EdgeQueue = new ArrayList<Edge>();//边集
    public static List<Vertex> newVertex = new ArrayList<Vertex>();//已经 访问过的结点

    public static void main(String[] args) {
        primTree();
    }
    public static void buildGraph() {
        Vertex v1 = new Vertex("a");
        Prim.vertexList.add(v1);
        Vertex v2 = new Vertex("b");
        Prim.vertexList.add(v2);
        Vertex v3 = new Vertex("c");
        Prim.vertexList.add(v3);
        Vertex v4 = new Vertex("d");
        Prim.vertexList.add(v4);
        Vertex v5 = new Vertex("e");
        Prim.vertexList.add(v5);
        addEdge(v1, v2, 6);
        addEdge(v1, v3, 7);
        addEdge(v2, v3, 8);
        addEdge(v2, v5, 4);
        addEdge(v2, v4, 5);
        addEdge(v3, v4, 3);
        addEdge(v3, v5, 9);
        addEdge(v5, v4, 7);
        addEdge(v5, v1, 2);
        addEdge(v4, v2, 2);
    }
    public static void addEdge(Vertex a, Vertex b, int w) {
        Edge e = new Edge(a, b, w);
        Prim.EdgeQueue.add(e);
    }
    public static void primTree() {
        buildGraph();
        Vertex start = vertexList.get(0);
        newVertex.add(start);
        for (int n = 0; n < vertexList.size() - 1; n++) {
            Vertex temp = new Vertex(start.key);
            Edge tempedge = new Edge(start, start, 1000);
            for (Vertex v : newVertex) {
                for (Edge e : EdgeQueue) {
                    if (e.start == v && !containVertex(e.end)) {
                        if (e.key < tempedge.key) {
                            temp = e.end;
                            tempedge = e;
                        }
                    }
                }
            }
            newVertex.add(temp);
        }
        Iterator it = newVertex.iterator();
        while (it.hasNext()) {
            Vertex v = (Vertex) it.next();
            System.out.println(v.key);
        }
    }
    public static boolean containVertex(Vertex vte) {
        for (Vertex v : newVertex) {
            if (v.key.equals(vte.key))
                return true;
        }
        return false;
    }
}

class Vertex {
    String key;
    Vertex(String key) {
        this.key = key;
    }
}

class Edge {
    Vertex start;
    Vertex end;
    int key;
    Edge(Vertex start, Vertex end, int key) {
        this.start = start;
        this.end  = end;
        this.key = key;
    }
}