跳转至

最小生成树

概述

最小生成树(Minimum Spanning Tree, MST)是连通加权图中**边权值之和最小的生成树**。生成树是包含图中所有顶点的无环连通子图。

最小生成树性质
  • 包含所有顶点:n 个顶点的生成树有 n-1 条边
  • 无环连通:是一棵树,没有回路
  • 权值最小:所有生成树中边权值之和最小
  • 可能不唯一:如果存在权值相同的边,MST 可能不唯一

生活类比

想象你要为 n 个城市铺设电缆,使得所有城市都能连通,且电缆总长度最短。这就是最小生成树问题——每个城市是顶点,城市间可能的电缆路线是边,电缆长度是权重。

问题定义

生成树 vs 最小生成树

Text Only
原图:
        4
    A ────── B
    │╲       │╲
   2│ ╲3    1│ ╲5
    ↓  ╲     ↓  ╲
    C───────D────E
        6       3
           F
        2

生成树示例(只是连通,不一定最小):
┌────────────────────────────────────────────────────┐
│ 树1: 边(A-B, A-C, B-D, D-E, E-F) 权值=4+2+1+3+3=13 │
│ 树2: 边(A-C, C-D, B-D, D-E, E-F) 权值=2+6+1+3+3=15 │
│ 树3: 边(A-B, A-D, A-C, D-E, E-F) 权值=4+3+2+3+3=15 │
└────────────────────────────────────────────────────┘

最小生成树(MST):
┌────────────────────────────────────────────────────┐
│ MST: 边(A-C, B-D, A-D, E-F, D-E)                   │
│      权值 = 2 + 1 + 3 + 3 + 2 = 11                │
│      这是最小的可能权值                            │
└────────────────────────────────────────────────────┘

MST 的切割性质

切割性质(Cut Property)

对于图的任意切割(将顶点分为两个非空集合),跨越切割的最小权重边一定在某个 MST 中。

这是 Prim 和 Kruskal 算法的理论基础。

graph TB
    subgraph "切割性质示意"
        subgraph "集合 S"
            A["A"]
            C["C"]
        end
        subgraph "集合 V-S"
            B["B"]
            D["D"]
        end
        
        A --"4"--> B
        A --"2"--> C
        C --"6"--> D
        A --"3"--> D
    end
    
    style A fill:#E3F2FD
    style C fill:#E3F2FD
    style B fill:#E8F5E9
    style D fill:#E8F5E9

Prim 算法

算法原理

Prim 算法从任意顶点开始,逐步扩展生成树,每次选择连接已选顶点集和未选顶点集的**最小权重边**。

flowchart TB
    subgraph "Prim 算法流程"
        A["选择任意起始顶点"] --> B["维护一个已选顶点集"]
        B --> C["找到连接已选集和未选集的最小边"]
        C --> D["将该边和对端顶点加入已选集"]
        D --> E{"所有顶点都已选?"}
        E -->|否| C
        E -->|是| F["算法结束"]
    end
    
    style A fill:#E3F2FD,stroke:#2196F3
    style F fill:#E8F5E9,stroke:#4CAF50

算法执行过程

Text Only
示例图:
        4
    A ────── B
    │╲       │
   2│ ╲3    1│
    ↓  ╲     ↓
    C───────D
        6

从顶点 A 开始执行 Prim 算法:

步骤1: 初始化
┌────────────────────────────────────────────────────┐
│ 已选顶点: {A}                                      │
│ 候选边: (A,B)=4, (A,C)=2, (A,D)=3                 │
│ key[]: A=0, B=4, C=2, D=3                         │
└────────────────────────────────────────────────────┘

步骤2: 选择最小 key 的未选顶点(C, key=2)
┌────────────────────────────────────────────────────┐
│ 选择边 (A,C), 权值 2                               │
│ 已选顶点: {A, C}                                   │
│ 更新候选边: C的邻居 D, 边(C,D)=6 > key[D]=3, 不更新│
│ key[]: A=0, B=4, C=2, D=3                         │
└────────────────────────────────────────────────────┘

步骤3: 选择最小 key 的未选顶点(D, key=3)
┌────────────────────────────────────────────────────┐
│ 选择边 (A,D), 权值 3                               │
│ 已选顶点: {A, C, D}                                │
│ D的邻居 B, 边(D,B)=1 < key[B]=4, 更新!            │
│ key[]: A=0, B=1, C=2, D=3                         │
└────────────────────────────────────────────────────┘

步骤4: 选择最小 key 的未选顶点(B, key=1)
┌────────────────────────────────────────────────────┐
│ 选择边 (D,B), 权值 1                               │
│ 已选顶点: {A, B, C, D}                             │
│ 所有顶点已选,算法结束                              │
└────────────────────────────────────────────────────┘

最终 MST:
┌────────────────────────────────────────────────────┐
│ 边: (A,C), (A,D), (D,B)                           │
│ 总权值: 2 + 3 + 1 = 6                             │
└────────────────────────────────────────────────────┘

算法状态转移图

stateDiagram-v2
    [*] --> 初始状态
    初始状态 --> 选择顶点A: key[A]=0
    选择顶点A --> 更新邻居: 松弛B,C,D
    更新邻居 --> 选择顶点C: key[C]=2最小
    选择顶点C --> 更新D的key: 检查边(C,D)
    更新D的key --> 选择顶点D: key[D]=3最小
    选择顶点D --> 更新B的key: 边(D,B)=1更小
    更新B的key --> 选择顶点B: key[B]=1最小
    选择顶点B --> 完成: 所有顶点已选
    完成 --> [*]

基本实现(邻接矩阵)

C
#include <stdio.h>
#include <limits.h>

#define V 4
#define INF INT_MAX

// 找到 key 值最小且未包含在 MST 中的顶点
int minKey(int key[], int mstSet[]) {
    int min = INF, minIndex = -1;
    
    for (int v = 0; v < V; v++) {
        if (!mstSet[v] && key[v] < min) {
            min = key[v];
            minIndex = v;
        }
    }
    
    return minIndex;
}

// 打印 MST
void printMST(int parent[], int graph[V][V]) {
    printf("边\t\t权值\n");
    int totalWeight = 0;
    for (int i = 1; i < V; i++) {
        printf("%c - %c\t\t%d\n", 
               'A' + parent[i], 'A' + i, graph[i][parent[i]]);
        totalWeight += graph[i][parent[i]];
    }
    printf("总权值: %d\n", totalWeight);
}

// Prim 算法
void primMST(int graph[V][V]) {
    int parent[V];   // MST 中各顶点的父节点
    int key[V];      // 连接到 MST 的最小权值
    int mstSet[V];   // 是否已加入 MST
    
    // 初始化
    for (int i = 0; i < V; i++) {
        key[i] = INF;
        mstSet[i] = 0;
        parent[i] = -1;
    }
    
    // 从顶点 0 开始
    key[0] = 0;
    parent[0] = -1;  // 第一个顶点是根
    
    printf("Prim 算法执行过程:\n\n");
    
    // 构建 MST
    for (int count = 0; count < V - 1; count++) {
        // 选择 key 最小的未选顶点
        int u = minKey(key, mstSet);
        mstSet[u] = 1;
        
        printf("步骤 %d: 选择顶点 %c\n", count + 1, 'A' + u);
        if (parent[u] != -1) {
            printf("  加入边: (%c, %c), 权值=%d\n", 
                   'A' + parent[u], 'A' + u, key[u]);
        }
        
        // 更新相邻顶点的 key 值
        for (int v = 0; v < V; v++) {
            if (graph[u][v] && !mstSet[v] && graph[u][v] < key[v]) {
                parent[v] = u;
                key[v] = graph[u][v];
                printf("  更新 key[%c] = %d\n", 'A' + v, key[v]);
            }
        }
        printf("\n");
    }
    
    printf("\n最小生成树:\n");
    printMST(parent, graph);
}

int main() {
    // 邻接矩阵
    int graph[V][V] = {
        //A    B    C    D
        {0,   4,   2,   3},   // A
        {4,   0, INF,   1},   // B
        {2, INF,   0,   6},   // C
        {3,   1,   6,   0}    // D
    };
    
    primMST(graph);
    
    return 0;
}

优先队列优化版本

graph LR
    subgraph "时间复杂度对比"
        A["朴素实现<br>O(V²)"] --> B["使用优先队列<br>O(E log V)"]
    end
    
    style B fill:#E8F5E9,stroke:#4CAF50
C
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

#define V 4
#define INF INT_MAX

typedef struct {
    int vertex;
    int key;
} HeapNode;

typedef struct {
    HeapNode *nodes;
    int size;
    int capacity;
} MinHeap;

// 创建最小堆
MinHeap* createMinHeap(int capacity) {
    MinHeap *heap = (MinHeap*)malloc(sizeof(MinHeap));
    heap->nodes = (HeapNode*)malloc(capacity * sizeof(HeapNode));
    heap->size = 0;
    heap->capacity = capacity;
    return heap;
}

// 交换
void swap(HeapNode *a, HeapNode *b) {
    HeapNode temp = *a;
    *a = *b;
    *b = temp;
}

// 最小堆化
void minHeapify(MinHeap *heap, int idx) {
    int smallest = idx;
    int left = 2 * idx + 1;
    int right = 2 * idx + 2;
    
    if (left < heap->size && 
        heap->nodes[left].key < heap->nodes[smallest].key)
        smallest = left;
    
    if (right < heap->size && 
        heap->nodes[right].key < heap->nodes[smallest].key)
        smallest = right;
    
    if (smallest != idx) {
        swap(&heap->nodes[idx], &heap->nodes[smallest]);
        minHeapify(heap, smallest);
    }
}

// 插入
void insertHeap(MinHeap *heap, int vertex, int key) {
    int i = heap->size++;
    heap->nodes[i].vertex = vertex;
    heap->nodes[i].key = key;
    
    while (i != 0 && 
           heap->nodes[(i-1)/2].key > heap->nodes[i].key) {
        swap(&heap->nodes[i], &heap->nodes[(i-1)/2]);
        i = (i - 1) / 2;
    }
}

// 提取最小
HeapNode extractMin(MinHeap *heap) {
    if (heap->size == 0) {
        HeapNode empty = {-1, INF};
        return empty;
    }
    
    HeapNode min = heap->nodes[0];
    heap->nodes[0] = heap->nodes[--heap->size];
    minHeapify(heap, 0);
    
    return min;
}

// 减小键值
void decreaseKey(MinHeap *heap, int vertex, int newKey) {
    int i;
    for (i = 0; i < heap->size; i++) {
        if (heap->nodes[i].vertex == vertex) {
            heap->nodes[i].key = newKey;
            break;
        }
    }
    
    while (i != 0 && 
           heap->nodes[(i-1)/2].key > heap->nodes[i].key) {
        swap(&heap->nodes[i], &heap->nodes[(i-1)/2]);
        i = (i - 1) / 2;
    }
}

// Prim 算法(优先队列优化)
void primOptimized(int graph[V][V]) {
    int parent[V];
    int key[V];
    int inMST[V] = {0};
    
    for (int i = 0; i < V; i++) {
        key[i] = INF;
        parent[i] = -1;
    }
    
    key[0] = 0;
    MinHeap *heap = createMinHeap(V);
    insertHeap(heap, 0, 0);
    
    printf("Prim 算法(优先队列优化):\n\n");
    
    while (heap->size > 0) {
        HeapNode min = extractMin(heap);
        int u = min.vertex;
        
        if (inMST[u]) continue;
        inMST[u] = 1;
        
        printf("选择顶点 %c, key=%d\n", 'A' + u, key[u]);
        
        for (int v = 0; v < V; v++) {
            if (graph[u][v] && !inMST[v] && graph[u][v] < key[v]) {
                key[v] = graph[u][v];
                parent[v] = u;
                insertHeap(heap, v, key[v]);
                printf("  更新: key[%c] = %d, parent[%c] = %c\n", 
                       'A' + v, key[v], 'A' + v, 'A' + u);
            }
        }
    }
    
    printf("\nMST 边:\n");
    for (int i = 1; i < V; i++) {
        printf("(%c, %c) = %d\n", 
               'A' + parent[i], 'A' + i, graph[i][parent[i]]);
    }
    
    free(heap->nodes);
    free(heap);
}

Kruskal 算法

算法原理

Kruskal 算法按照**边权值从小到大**的顺序选择边,每次选择不会形成环的边,直到选出 n-1 条边。

flowchart TB
    subgraph "Kruskal 算法流程"
        A["将所有边按权值排序"] --> B["初始化并查集<br>每个顶点独立"]
        B --> C["选择最小权值的边"]
        C --> D{"该边的两端<br>在同一集合?"}
        D -->|是| E["跳过(会形成环)"]
        D -->|否| F["加入该边<br>合并两个集合"]
        E --> G{"已选 n-1 条边?"}
        F --> G
        G -->|否| C
        G -->|是| H["算法结束"]
    end
    
    style A fill:#E3F2FD,stroke:#2196F3
    style H fill:#E8F5E9,stroke:#4CAF50
    style E fill:#FFF3E0,stroke:#FF9800

算法执行过程

Text Only
示例图(同上):
        4
    A ────── B
    │╲       │
   2│ ╲3    1│
    ↓  ╲     ↓
    C───────D
        6

步骤1: 边排序
┌────────────────────────────────────────────────────┐
│ 按权值排序的边:                                    │
│ 1. (B,D) = 1                                      │
│ 2. (A,C) = 2                                      │
│ 3. (A,D) = 3                                      │
│ 4. (A,B) = 4                                      │
│ 5. (C,D) = 6                                      │
└────────────────────────────────────────────────────┘

初始并查集: {A}, {B}, {C}, {D}

步骤2: 选择边 (B,D) = 1
┌────────────────────────────────────────────────────┐
│ B 和 D 在不同集合 → 接受                           │
│ 合并: {A}, {B,D}, {C}                             │
│ MST 边: {(B,D)}                                   │
└────────────────────────────────────────────────────┘

步骤3: 选择边 (A,C) = 2
┌────────────────────────────────────────────────────┐
│ A 和 C 在不同集合 → 接受                           │
│ 合并: {A,C}, {B,D}                                │
│ MST 边: {(B,D), (A,C)}                            │
└────────────────────────────────────────────────────┘

步骤4: 选择边 (A,D) = 3
┌────────────────────────────────────────────────────┐
│ A 在 {A,C}, D 在 {B,D} → 不同集合 → 接受           │
│ 合并: {A,B,C,D}                                   │
│ MST 边: {(B,D), (A,C), (A,D)}                     │
│ 已选 3 条边 = V-1 → 算法结束                      │
└────────────────────────────────────────────────────┘

最终 MST:
┌────────────────────────────────────────────────────┐
│ 边: (B,D), (A,C), (A,D)                           │
│ 总权值: 1 + 2 + 3 = 6                             │
│ (与 Prim 结果相同,只是边的顺序不同)              │
└────────────────────────────────────────────────────┘

环检测示意

Text Only
为什么不选择边 (A,B) = 4?

假设已经选择了 (B,D), (A,C), (A,D):
┌────────────────────────────────────────────────────┐
│                                                    │
│       A ─────────── D                              │
│       │╲            │                              │
│       │ ╲           │                              │
│       │  ╲          │                              │
│       C             B                              │
│                                                    │
│ 如果再选 (A,B):                                    │
│ A → D → B → A 形成环!                             │
│ 所以跳过边 (A,B)                                   │
└────────────────────────────────────────────────────┘

基本实现

C
#include <stdio.h>
#include <stdlib.h>

#define V 4

typedef struct {
    int src, dest, weight;
} Edge;

// 并查集
int parent[V];
int rank[V];

// 初始化并查集
void initUnionFind() {
    for (int i = 0; i < V; i++) {
        parent[i] = i;
        rank[i] = 0;
    }
}

// 查找(带路径压缩)
int find(int i) {
    if (parent[i] != i) {
        parent[i] = find(parent[i]);  // 路径压缩
    }
    return parent[i];
}

// 合并(按秩合并)
int unionSets(int i, int j) {
    int rootI = find(i);
    int rootJ = find(j);
    
    if (rootI == rootJ) {
        return 0;  // 已经在同一集合
    }
    
    // 按秩合并
    if (rank[rootI] < rank[rootJ]) {
        parent[rootI] = rootJ;
    } else if (rank[rootI] > rank[rootJ]) {
        parent[rootJ] = rootI;
    } else {
        parent[rootJ] = rootI;
        rank[rootI]++;
    }
    
    return 1;  // 合并成功
}

// 边比较函数(用于排序)
int compareEdges(const void *a, const void *b) {
    return ((Edge*)a)->weight - ((Edge*)b)->weight;
}

// Kruskal 算法
void kruskalMST(Edge edges[], int E) {
    // 按权值排序边
    qsort(edges, E, sizeof(Edge), compareEdges);
    
    printf("排序后的边:\n");
    for (int i = 0; i < E; i++) {
        printf("%d. (%c,%c) = %d\n", i + 1, 
               'A' + edges[i].src, 'A' + edges[i].dest, 
               edges[i].weight);
    }
    printf("\n");
    
    initUnionFind();
    
    Edge result[V - 1];
    int e = 0;  // 已选边数
    int i = 0;  // 当前检查的边索引
    
    printf("Kruskal 算法执行过程:\n\n");
    
    while (e < V - 1 && i < E) {
        Edge next = edges[i++];
        
        printf("检查边 (%c,%c) = %d\n", 
               'A' + next.src, 'A' + next.dest, next.weight);
        
        int rootSrc = find(next.src);
        int rootDest = find(next.dest);
        
        if (rootSrc != rootDest) {
            // 不形成环,接受该边
            result[e++] = next;
            unionSets(next.src, next.dest);
            printf("  接受!合并集合\n");
            
            printf("  当前集合: ");
            for (int j = 0; j < V; j++) {
                printf("{");
                for (int k = 0; k < V; k++) {
                    if (find(k) == j) printf("%c ", 'A' + k);
                }
                printf("} ");
            }
            printf("\n\n");
        } else {
            printf("  拒绝!会形成环\n\n");
        }
    }
    
    // 输出结果
    printf("最小生成树:\n");
    printf("边\t\t权值\n");
    int totalWeight = 0;
    for (i = 0; i < e; i++) {
        printf("(%c,%c)\t\t%d\n", 
               'A' + result[i].src, 'A' + result[i].dest, 
               result[i].weight);
        totalWeight += result[i].weight;
    }
    printf("总权值: %d\n", totalWeight);
}

int main() {
    // 边列表
    Edge edges[] = {
        {0, 1, 4},  // A-B
        {0, 2, 2},  // A-C
        {0, 3, 3},  // A-D
        {1, 3, 1},  // B-D
        {2, 3, 6}   // C-D
    };
    int E = sizeof(edges) / sizeof(edges[0]);
    
    kruskalMST(edges, E);
    
    return 0;
}

Prim vs Kruskal 对比

graph TB
    subgraph "Prim 算法"
        A1["顶点优先"] --> B1["逐步扩展生成树"]
        B1 --> C1["适合稠密图<br>O(V²) 或 O(E log V)"]
        C1 --> D1["数据结构: 优先队列"]
    end
    
    subgraph "Kruskal 算法"
        A2["边优先"] --> B2["全局选择最小边"]
        B2 --> C2["适合稀疏图<br>O(E log E)"]
        C2 --> D2["数据结构: 并查集"]
    end
    
    style C1 fill:#E3F2FD,stroke:#2196F3
    style C2 fill:#E8F5E9,stroke:#4CAF50

详细对比

Text Only
┌─────────────────────────────────────────────────────────────────────────────┐
│ 特性           │ Prim                    │ Kruskal                       │
├─────────────────────────────────────────────────────────────────────────────┤
│ 选择策略       │ 顶点优先,局部扩展       │ 边优先,全局选择              │
│ 时间复杂度     │ O(V²) 或 O(E log V)     │ O(E log E)                    │
│ 数据结构       │ 优先队列(最小堆)       │ 并查集                        │
│ 预处理         │ 无需排序                 │ 需要排序所有边                │
│ 适用图类型     │ 稠密图                   │ 稀疏图                        │
│ 空间复杂度     │ O(V)                     │ O(E)                          │
│ 实现复杂度     │ 中等                     │ 简单(使用现成并查集)        │
│ 图表示         │ 邻接矩阵或邻接表         │ 边列表                        │
├─────────────────────────────────────────────────────────────────────────────┤
│ 稠密图(E≈V²)   │ O(V² log V)             │ O(V² log V)                   │
│ 稀疏图(E≈V)    │ O(V log V)              │ O(V log V)                    │
└─────────────────────────────────────────────────────────────────────────────┘

选择建议

算法选择建议
  • 稠密图(E ≈ V²):Prim 朴素版本 O(V²) 更优
  • 稀疏图(E ≈ V):Kruskal 或 Prim 堆优化版本更优
  • 只有边列表:使用 Kruskal
  • 需要增量构建:使用 Prim

Boruvka 算法

算法原理

Boruvka 算法适合并行计算,每轮为每个连通分量选择最小出边。

flowchart TB
    subgraph "Boruvka 算法流程"
        A["初始化: 每个顶点独立分量"] --> B["每轮并行处理"]
        B --> C["为每个分量找最小出边"]
        C --> D["合并分量"]
        D --> E{"只剩一个分量?"}
        E -->|否| B
        E -->|是| F["算法结束"]
    end
    
    style F fill:#E8F5E9,stroke:#4CAF50
C
void boruvkaMST(Edge edges[], int E, int V) {
    int parent[V];
    int rank[V];
    
    // 初始化并查集
    for (int i = 0; i < V; i++) {
        parent[i] = i;
        rank[i] = 0;
    }
    
    int numComponents = V;
    int totalWeight = 0;
    
    printf("Boruvka 算法:\n\n");
    
    while (numComponents > 1) {
        // 存储每个分量的最小出边
        int cheapest[V];
        for (int i = 0; i < V; i++) {
            cheapest[i] = -1;
        }
        
        // 为每个分量找最小出边
        for (int i = 0; i < E; i++) {
            int set1 = find(edges[i].src);
            int set2 = find(edges[i].dest);
            
            if (set1 != set2) {
                // 更新 set1 的最小出边
                if (cheapest[set1] == -1 || 
                    edges[cheapest[set1]].weight > edges[i].weight) {
                    cheapest[set1] = i;
                }
                // 更新 set2 的最小出边
                if (cheapest[set2] == -1 || 
                    edges[cheapest[set2]].weight > edges[i].weight) {
                    cheapest[set2] = i;
                }
            }
        }
        
        // 添加选中的边
        printf("本轮选择的边:\n");
        for (int i = 0; i < V; i++) {
            if (cheapest[i] != -1) {
                int set1 = find(edges[cheapest[i]].src);
                int set2 = find(edges[cheapest[i]].dest);
                
                if (set1 != set2) {
                    unionSets(set1, set2);
                    printf("  (%c,%c) = %d\n", 
                           'A' + edges[cheapest[i]].src,
                           'A' + edges[cheapest[i]].dest,
                           edges[cheapest[i]].weight);
                    totalWeight += edges[cheapest[i]].weight;
                    numComponents--;
                }
            }
        }
        printf("\n");
    }
    
    printf("MST 总权值: %d\n", totalWeight);
}

应用场景

1. 网络设计

C
// 最低成本网络布线
typedef struct {
    char name[50];
    int x, y;  // 坐标
} Node;

void designNetwork(Node nodes[], int n) {
    // 计算所有节点对之间的距离
    int graph[MAX][MAX];
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            if (i != j) {
                int dx = nodes[i].x - nodes[j].x;
                int dy = nodes[i].y - nodes[j].y;
                graph[i][j] = (int)sqrt(dx*dx + dy*dy);
            } else {
                graph[i][j] = 0;
            }
        }
    }
    
    // 使用 Prim 或 Kruskal 求最小生成树
    primMST(graph);
    
    printf("网络设计方案:\n");
    printf("总布线长度最小\n");
}

2. 电路设计

C
1
2
3
4
5
6
// PCB 布线优化
void optimizePCBRouting(Component components[], int n) {
    // 建立连接图
    // 边权重 = 布线成本
    // MST = 最低成本连接方案
}

3. 图像分割

C
// 基于最小生成树的图像分割
typedef struct {
    int pixel1, pixel2;
    int weight;  // 像素差异
} PixelEdge;

void imageSegmentation(PixelEdge edges[], int E, int threshold) {
    // 对边按权重排序
    qsort(edges, E, sizeof(PixelEdge), compareEdges);
    
    // Kruskal 过程中,如果边权重超过阈值,不合并
    // 形成多个区域
}

4. 聚类算法

C
1
2
3
4
5
6
// 层次聚类
void hierarchicalClustering(Point points[], int n, int k) {
    // 计算所有点对距离
    // 求最小生成树
    // 删除 k-1 条最长边,得到 k 个聚类
}

复杂度总结

算法 时间复杂度 空间复杂度 最佳场景
Prim(朴素) O(V²) O(V) 稠密图
Prim(堆优化) O(E log V) O(V) 一般图
Kruskal O(E log E) O(E) 稀疏图
Boruvka O(E log V) O(V) 并行计算

参考资料