最小生成树

参考资料

水平有限,欢迎交流
【图-最小生成树-Prim (普里姆) 算法和 Kruskal (克鲁斯卡尔) 算法】
【Kruskal一往无前,并查集鼎力相助(算法童话第一回)】
【Prim稳扎稳打,最小堆暗中相助(算法童话第二回)】

练习题

P3366 【模板】最小生成树 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
leetcode1584
Kruskal(克鲁斯卡尔)算法和 Prim(普里姆)算法都是用于寻找图中最小生成树(Minimum Spanning Tree, MST)的经典算法。这两种算法虽然目的相同,但在实现细节、效率和适用场景上有所不同。下面简要概述两种算法的主要优点和缺点:

Kruskal 算法(找最小的边)

优点

  1. 简单易懂:Kruskal 算法的逻辑相对直接,易于理解和实现。
  2. 适用于稀疏图:当图中的边数远小于节点数的平方时,Kruskal 算法通常比 Prim 算法更高效,因为它的时间复杂度主要取决于边的数量。
  3. 并行处理友好:由于 Kruskal 算法在每一步中可以独立地选择最短的边,因此它非常适合并行处理。
    缺点
  4. 需要额外的数据结构:为了检测环路,Kruskal 算法通常需要使用并查集(Union-Find)等数据结构,这增加了算法的空间复杂度。
  5. 对于稠密图效率较低:当图非常密集时,即边的数量接近于节点数量的平方时,Kruskal 算法的效率会低于 Prim 算法。
//路径压缩并查集
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;

class Edge implements Comparable<Edge>{
    int src;
    int dest;
    int w;
    Edge(int src,int dest,int w){
        this.src = src;
        this.dest = dest;
        this.w = w;
    }
    @Override
    public int compareTo(Edge o){
        return this.w-o.w;
    }
}
class UnionFind {
    private int []parent;
    UnionFind(int v){
        parent = new int[v];
        for (int i = 0; i < v; i++) {
            parent[i] = i;
        }
    }
    int find(int n){
        if(parent[n]!=n){
            parent[n] = find(parent[n]);
        }
        return parent[n];
    }
    void union(int x,int y){
        parent[find(x)] = find(y);
    }
}
class KruskalMST{
    private int v;
    private Edge[]edges;
    private UnionFind uf;
    KruskalMST(int v,Edge[]edges){
        this.v = v;
        this.edges = edges;
        uf = new UnionFind(v);
    }
    void kruskalMST(){
        int e = 0;
        int res = 0;
        Arrays.sort(edges);
        while(e<edges.length){
            Edge nextEdge = edges[e++];
            int src = nextEdge.src;
            int dest = nextEdge.dest;
            int w = nextEdge.w;

            int x = uf.find(src);
            int y = uf.find(dest);
            if(x!=y){
                uf.union(x, y);
                res+=w;
            }
        }
        for(int v = 1;v<this.v;v++){
            if(uf.find(v)!=uf.find(0)){
                System.out.println("orz");
                return;
            }
        }
        System.out.println(res);
    }
}
public class Main {
    static int n,m;
    public static void main(String[] args) throws IOException{
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String []line = in.readLine().split(" ");
        n = Integer.parseInt(line[0]);
        m = Integer.parseInt(line[1]);
        Edge []edges = new Edge[m];
        for (int i = 0; i < m; i++) {
            line = in.readLine().split(" ");
            int x = Integer.parseInt(line[0])-1;
            int y = Integer.parseInt(line[1])-1;
            int z = Integer.parseInt(line[2]);
            edges[i] = new Edge(x, y, z);
        }
        KruskalMST k = new KruskalMST(n,edges);
        k.kruskalMST();
        in.close();
    }
}

Prim 算法(找最小的顶点)

邻接矩阵写法

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;

class Graph {
    private int v;
    private int[][] g;
    private boolean[] flag;
    private int[] key;

    Graph(int v) {
        this.v = v;
        g = new int[v][v];
    }

    void add(int v1, int v2, int w) {
        if (v1 == v2)
            return; // 自循环检查
        if (g[v1][v2] == 0 || w < g[v1][v2]) {//重复边处理
            g[v1][v2] = g[v2][v1] = w; // 更新为最小权重
        }
    }

    int getMinV() {
        int min = -1;
        int minVal = Integer.MAX_VALUE;
        for (int i = 0; i < this.v; i++) {
            if (!flag[i] && key[i] < minVal) {
                minVal = key[i];
                min = i;
            }
        }
        return min;
    }

    void Prim() {
        flag = new boolean[this.v];
        key = new int[this.v];
        Arrays.fill(key, Integer.MAX_VALUE);
        key[0] = 0;

        for (int i = 0; i < this.v; i++) {
            int u = getMinV();
            if (u == -1) {
                break;
            }
            flag[u] = true; // 更新标志位

            for (int v = 0; v < this.v; v++) {
                if (g[u][v] != 0 && !flag[v] && g[u][v] < key[v]) {
                    key[v] = g[u][v];
                }
            }
        }

        long res = 0;
        for (int i = 1; i < this.v; i++) {
            if (!flag[i]) {
                System.out.println("orz");
                return;
            }
            res += key[i];
        }
        System.out.println(res);
    }
}

public class Main {
    static int n, m;

    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String[] line = in.readLine().split(" ");
        n = Integer.parseInt(line[0]);
        m = Integer.parseInt(line[1]);
        Graph g = new Graph(n);

        for (int i = 0; i < m; i++) {
            line = in.readLine().split(" ");
            int v1 = Integer.parseInt(line[0]) - 1;
            int v2 = Integer.parseInt(line[1]) - 1;
            int w = Integer.parseInt(line[2]);
            g.add(v1, v2, w);
        }

        g.Prim();
        in.close();
    }
}

邻接矩阵(堆优化写法)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.PriorityQueue;

class Edge implements Comparable<Edge> {
    int vertex;
    int weight;

    public Edge(int v, int w) {
        this.vertex = v;
        this.weight = w;
    }

    @Override
    public int compareTo(Edge other) {
        return Integer.compare(this.weight, other.weight);
    }
}

class Graph {
    private int v;
    private int[][] g;
    private boolean[] flag;
    private int[] key;
    private PriorityQueue<Edge> pq;

    Graph(int v) {
        this.v = v;
        g = new int[v][v];
        pq = new PriorityQueue<>();
    }

    void add(int v1, int v2, int w) {
        if (v1 == v2)
            return; // 自循环检查
        if (g[v1][v2] == 0 || w < g[v1][v2]) {
            g[v1][v2] = g[v2][v1] = w; // 更新为最小权重
        }
    }

    void Prim() {
        flag = new boolean[this.v];
        key = new int[this.v];
        Arrays.fill(key, Integer.MAX_VALUE);
        key[0] = 0;

        // 将起点加入优先队列
        pq.offer(new Edge(0, 0));

        while (!pq.isEmpty()) {
            Edge current = pq.poll();
            int u = current.vertex;
            if (flag[u]) continue; // 如果已经处理过这个节点,则跳过

            flag[u] = true; // 标记为已访问

            for (int v = 0; v < this.v; v++) {
                if (g[u][v] != 0 && !flag[v] && g[u][v] < key[v]) {
                    key[v] = g[u][v];
                    pq.offer(new Edge(v, key[v])); // 将新的候选边加入优先队列
                }
            }
        }

        long res = 0;
        for (int i = 1; i < this.v; i++) {
            if (!flag[i]) {
                System.out.println("orz");
                return;
            }
            res += key[i];
        }
        System.out.println(res);
    }
}

public class Main {
    static int n, m;

    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String[] line = in.readLine().split(" ");
        n = Integer.parseInt(line[0]);
        m = Integer.parseInt(line[1]);
        Graph g = new Graph(n);

        for (int i = 0; i < m; i++) {
            line = in.readLine().split(" ");
            int v1 = Integer.parseInt(line[0]) - 1;
            int v2 = Integer.parseInt(line[1]) - 1;
            int w = Integer.parseInt(line[2]);
            g.add(v1, v2, w);
        }

        g.Prim();
        in.close();
    }
}

邻接表写法 (适合稀疏图)

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
class Node {
    int nei;
    int w;
    Node(int nei,int w){
        this.nei = nei;
        this.w = w;
    }
}
class Graph {
    private int v;
    private ArrayList<ArrayList<Node>> list;
    private boolean []flag;
    private int[]key;
    Graph(int v){
        this.v = v;
        list = new ArrayList<>();
        for(int i = 0;i<=v;i++){
            list.add(new ArrayList<>());
        }
    }
    int getMinV(){
        int min = -1;
        int minVal = Integer.MAX_VALUE;
        for(int i = 1;i<=this.v;i++){
            if(!flag[i] && key[i]<minVal){
                minVal = key[i];
                min = i;
            }
        }
        return min;
    }
    void add(int v1, int v2, int w) {
        // 避免自环边
        if (v1 == v2) {
            return;
        }
        list.get(v1).add(new Node(v2, w));
        list.get(v2).add(new Node(v1, w));
    }
    void Prim(){
        flag = new boolean[this.v + 1];
        key = new int[this.v+1];
        Arrays.fill(key, Integer.MAX_VALUE);
        key[1] = 0;
        for(int i = 0;i<this.v;i++){
            int u = getMinV();
            if(u == -1){
                break;
            }
            flag[u] = true;
            for (Node n : list.get(u)) {
                int v = n.nei;
                if(!flag[v] && n.w<key[v]){
                    key[v] = n.w;
                }
            }
        }
        long res = 0;
        for(int i = 2;i<=this.v;i++){
            if(!flag[i]){
                System.out.println("orz");
                return;
            }
            res+=key[i];
        }
        System.out.println(res);
    }

}
public class Main {
    static int n,m;

    public static void main(String[] args) throws IOException{
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String []line = in.readLine().split(" ");
        n = Integer.parseInt(line[0]);
        m = Integer.parseInt(line[1]);
        Graph g = new Graph(n);
        for(int i = 0;i<m;i++){
            line = in.readLine().split(" ");
            int v1 = Integer.parseInt(line[0]);
            int v2 = Integer.parseInt(line[1]);
            int w = Integer.parseInt(line[2]);
            g.add(v1, v2, w);
        }
        g.Prim();
        in.close();
    }
}

邻接表堆优化写法

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.PriorityQueue;

// 定义图的节点,用于存储相邻节点信息和边的权重
class Node implements Comparable<Node> {
    int nei; // 相邻节点
    int w; // 边的权重

    // 构造函数
    Node(int nei, int w) {
        this.nei = nei;
        this.w = w;
    }

    // 实现Comparable接口的compareTo方法,按照边的权重升序排序
    @Override
    public int compareTo(Node other) {
        return this.w - other.w;
    }
}

// 表示图的类
class Graph {
    private int v; // 图中节点的数量
    List<List<Node>> adjList; // 邻接表,存储图的结构
    PriorityQueue<Node> pq; // 优先队列,用于Prim算法中选择最小权重的边

    // 构造函数,初始化图
    Graph(int v) {
        this.v = v;
        adjList = new ArrayList<>();
        for (int i = 0; i < v; i++) {
            adjList.add(new ArrayList<>());
        }
        pq = new PriorityQueue<>();
    }

    // 添加边到图中
    void add(int v1, int v2, int w) {
        if (v1 == v2)// 自环边
            return;
        adjList.get(v1).add(new Node(v2, w));
        adjList.get(v2).add(new Node(v1, w));
    }

    // Prim算法,用于寻找最小生成树的权值和
    void prim() {
        boolean[] flag = new boolean[this.v]; // 标记节点是否已访问
        int[] key = new int[this.v]; // 存储节点到最小生成树的最小权值
        Arrays.fill(key, Integer.MAX_VALUE); // 初始化key数组
        pq.offer(new Node(0, 0)); // 将起始节点加入优先队列
        key[0] = 0; // 起始节点到自身的权值为0

        // Prim算法主循环
        while (!pq.isEmpty()) {
            Node n = pq.poll(); // 从优先队列中取出权值最小的节点
            int u = n.nei;
            if (flag[u])
                continue;
            flag[u] = true; // 标记节点u为已访问
            for (Node node : adjList.get(u)) {
                int v = node.nei;
                int w = node.w;
                if (!flag[v] && w < key[v]) {
                    key[v] = w; // 更新节点v到最小生成树的最小权值
                    pq.offer(node); // 将节点v加入优先队列
                }
            }
        }

        long res = 0; // 最小生成树的权值和
        for (int i = 1; i < this.v; i++) {
            if (!flag[i]) {
                System.out.println("orz"); // 如果有节点未被访问,表示图不连通
                return;
            }
            res += key[i]; // 累加权值
        }
        System.out.println(res); // 输出最小生成树的权值和
    }
}

public class Main {
    static int n, m;

    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String[] line = in.readLine().split(" ");
        n = Integer.parseInt(line[0]); // 读取节点数量
        m = Integer.parseInt(line[1]); // 读取边的数量
        Graph g = new Graph(n); // 创建图对象

        // 读取并添加图的边
        for (int i = 0; i < m; i++) {
            line = in.readLine().split(" ");
            int v1 = Integer.parseInt(line[0]) - 1;
            int v2 = Integer.parseInt(line[1]) - 1;
            int w = Integer.parseInt(line[2]);
            g.add(v1, v2, w);
        }

        g.prim(); // 执行Prim算法
        in.close(); // 关闭输入流
    }
}

优点

  1. 更适合稠密图:对于边数较多的图,Prim 算法通常表现得更好,因为它的性能主要依赖于节点数而不是边数。
  2. 不需要额外的数据结构来检测环路:Prim 算法通过维护一个包含已加入 MST 的节点集合来避免形成环路,因此不需要像 Kruskal 算法那样使用额外的数据结构。
  3. 构建过程连续:从任一顶点开始,逐步添加邻接顶点到当前的 MST 中,构建过程比较直观。
    缺点
  4. 实现相对复杂:相比于 Kruskal 算法,Prim 算法的实现稍微复杂一些,特别是在处理图的动态更新时。
  5. 难以并行化:由于 Prim 算法在每一步的选择都依赖于之前的选择结果,因此不太适合并行计算环境。

总结

  • 如果图是稀疏的,或者你希望算法能够更容易地并行化,那么 Kruskal 算法可能是一个更好的选择。
  • 对于稠密图或需要快速构建最小生成树的情况,Prim 算法通常更有效率。
posted @ 2024-10-05 15:50  yuanyulinyi  阅读(6)  评论(0编辑  收藏  举报