7-5 普里姆最小成本生成树算法

克鲁斯卡尔最小生成树算法(Kruskal's Minimum Cost Spanning Tree)

克鲁斯卡尔算法(Kruskal's Algorithm)是一种求解最小生成树(Minimum Spanning Tree,简称 MST)的经典贪心算法(Greedy Algorithm)。其核心思想是:将图中所有边按权重从小到大排序,然后依次考虑每条边——如果这条边连接的两个顶点属于不同的连通分量,就将这条边加入生成树中;否则跳过。使用并查集(Union-Find / Disjoint Set Union,简称 DSU)数据结构来高效判断两个顶点是否属于同一连通分量。

Kruskal 算法的贪心策略直觉上很自然:每次都选当前最短的且不会形成环的边,最终所有选中的边恰好构成一棵连接所有顶点的总权重最小的树。

本文使用如下加权无向图(9 个顶点,14 条边):

边列表(按权重排序):
(0,1,4)  (0,7,8)  (1,2,8)  (1,7,11) (2,3,7)  (2,5,4)
(2,8,2)  (3,4,9)  (3,5,14) (4,5,10) (5,6,2)  (6,7,1)
(6,8,6)  (7,8,7)

格式: (u, v, weight)

图的结构:
    0---4---1---8---2
    |      /|     / |
    8   11 8|   7/  |2
    |  /    |  /    |
    7       | 3     8
    |       |/      |
    1---6---5---2---+
    |      /|
    6   14 10
    |  /    |
    6       4
    7       5

MST 权重 = 37
MST 边: (6,7,1) (2,8,2) (5,6,2) (0,1,4) (2,5,4) (2,3,7) (0,7,8) (3,4,9)

边的表示与排序

Kruskal 算法的输入是一组边(Edge),每条边由两个端点和权重组成。算法的第一步是将所有边按权重从小到大排序。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

struct Edge {
    int u, v, weight;
};

// Sort edges by weight (ascending)
bool compareEdges(const Edge& a, const Edge& b) {
    return a.weight < b.weight;
}

int main() {
    vector<Edge> edges = {
        {0, 1, 4},  {0, 7, 8},  {1, 2, 8},  {1, 7, 11},
        {2, 3, 7},  {2, 5, 4},  {2, 8, 2},  {3, 4, 9},
        {3, 5, 14}, {4, 5, 10}, {5, 6, 2},  {6, 7, 1},
        {6, 8, 6},  {7, 8, 7},
    };

    // Sort edges by weight
    sort(edges.begin(), edges.end(), compareEdges);

    cout << "Edges sorted by weight:" << endl;
    for (const Edge& e : edges) {
        cout << "(" << e.u << "," << e.v << "," << e.weight << ")" << endl;
    }

    return 0;
}
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int u, v, weight;
} Edge;

// Compare function for qsort (ascending by weight)
int compareEdges(const void* a, const void* b) {
    Edge* ea = (Edge*)a;
    Edge* eb = (Edge*)b;
    return ea->weight - eb->weight;
}

int main() {
    Edge edges[] = {
        {0, 1, 4},  {0, 7, 8},  {1, 2, 8},  {1, 7, 11},
        {2, 3, 7},  {2, 5, 4},  {2, 8, 2},  {3, 4, 9},
        {3, 5, 14}, {4, 5, 10}, {5, 6, 2},  {6, 7, 1},
        {6, 8, 6},  {7, 8, 7},
    };
    int edgeCount = 14;

    // Sort edges by weight using qsort
    qsort(edges, edgeCount, sizeof(Edge), compareEdges);

    printf("Edges sorted by weight:\n");
    for (int i = 0; i < edgeCount; i++) {
        printf("(%d,%d,%d)\n", edges[i].u, edges[i].v, edges[i].weight);
    }

    return 0;
}
def main():
    edges = [
        (0, 1, 4),  (0, 7, 8),  (1, 2, 8),  (1, 7, 11),
        (2, 3, 7),  (2, 5, 4),  (2, 8, 2),  (3, 4, 9),
        (3, 5, 14), (4, 5, 10), (5, 6, 2),  (6, 7, 1),
        (6, 8, 6),  (7, 8, 7),
    ]

    # Sort edges by weight (3rd element)
    edges.sort(key=lambda e: e[2])

    print("Edges sorted by weight:")
    for e in edges:
        print(f"({e[0]},{e[1]},{e[2]})")

if __name__ == "__main__":
    main()
package main

import (
    "fmt"
    "sort"
)

type Edge struct {
    U, V, Weight int
}

func main() {
    edges := []Edge{
        {0, 1, 4},  {0, 7, 8},  {1, 2, 8},  {1, 7, 11},
        {2, 3, 7},  {2, 5, 4},  {2, 8, 2},  {3, 4, 9},
        {3, 5, 14}, {4, 5, 10}, {5, 6, 2},  {6, 7, 1},
        {6, 8, 6},  {7, 8, 7},
    }

    // Sort edges by weight using sort.Slice
    sort.Slice(edges, func(i, j int) bool {
        return edges[i].Weight < edges[j].Weight
    })

    fmt.Println("Edges sorted by weight:")
    for _, e := range edges {
        fmt.Printf("(%d,%d,%d)\n", e.U, e.V, e.Weight)
    }
}

上述代码将所有边按权重从小到大排序。C++ 使用 sort() 配合自定义比较函数;C 语言使用 qsort() 标准库函数;Python 使用 list.sort(key=...) 按 third 元素排序;Go 使用 sort.Slice() 配合匿名比较函数。排序是 Kruskal 算法的第一步,保证了贪心策略按权重递增的顺序选边。

运行该程序将输出:

Edges sorted by weight:
(6,7,1)
(2,8,2)
(5,6,2)
(0,1,4)
(2,5,4)
(6,8,6)
(2,3,7)
(7,8,7)
(0,7,8)
(3,4,9)
(4,5,10)
(1,2,8)
(1,7,11)
(3,5,14)

并查集(Union-Find)实现

并查集(Union-Find / Disjoint Set Union,简称 DSU)是 Kruskal 算法的关键数据结构。它支持两个核心操作:

  • Find(x):查找元素 x 所属集合的代表元素(根节点)
  • Union(x, y):将元素 x 和 y 所属的两个集合合并

为了提高效率,使用两种优化技术:

  • 路径压缩(Path Compression):在 Find 操作中,将沿途经过的所有节点直接指向根节点,使后续查询更快
  • 按秩合并(Union by Rank):在 Union 操作中,将高度较小的树挂到高度较大的树下,避免树退化成链表

经过这两种优化,每次操作的时间复杂度接近 O(1)(准确说是 O(alpha(n)),其中 alpha 是反阿克曼函数(Inverse Ackermann Function),增长极其缓慢)。

C++ 实现

#include <iostream>
#include <vector>
using namespace std;

class UnionFind {
private:
    vector<int> parent;
    vector<int> rank;

public:
    UnionFind(int n) {
        parent.resize(n);
        rank.resize(n, 0);
        // Each node is its own parent initially
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
    }

    // Find with path compression
    int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);  // Path compression
        }
        return parent[x];
    }

    // Union by rank
    bool unionSets(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);

        if (rootX == rootY) {
            return false;  // Already in the same set
        }

        // Attach smaller rank tree under root of larger rank tree
        if (rank[rootX] < rank[rootY]) {
            parent[rootX] = rootY;
        } else if (rank[rootX] > rank[rootY]) {
            parent[rootY] = rootX;
        } else {
            parent[rootY] = rootX;
            rank[rootX]++;
        }
        return true;
    }
};

int main() {
    UnionFind uf(5);

    // Test union and find operations
    uf.unionSets(0, 1);
    uf.unionSets(2, 3);
    uf.unionSets(0, 2);

    cout << "find(0) = " << uf.find(0) << endl;
    cout << "find(1) = " << uf.find(1) << endl;
    cout << "find(2) = " << uf.find(2) << endl;
    cout << "find(3) = " << uf.find(3) << endl;
    cout << "find(4) = " << uf.find(4) << endl;

    cout << "0 and 3 in same set: " << (uf.find(0) == uf.find(3) ? "yes" : "no") << endl;
    cout << "0 and 4 in same set: " << (uf.find(0) == uf.find(4) ? "yes" : "no") << endl;

    return 0;
}

C 实现

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

typedef struct {
    int* parent;
    int* rank;
    int n;
} UnionFind;

UnionFind* ufCreate(int n) {
    UnionFind* uf = (UnionFind*)malloc(sizeof(UnionFind));
    uf->n = n;
    uf->parent = (int*)malloc(n * sizeof(int));
    uf->rank = (int*)calloc(n, sizeof(int));
    // Each node is its own parent initially
    for (int i = 0; i < n; i++) {
        uf->parent[i] = i;
    }
    return uf;
}

// Find with path compression
int ufFind(UnionFind* uf, int x) {
    if (uf->parent[x] != x) {
        uf->parent[x] = ufFind(uf, uf->parent[x]);  // Path compression
    }
    return uf->parent[x];
}

// Union by rank, returns true if merged
int ufUnion(UnionFind* uf, int x, int y) {
    int rootX = ufFind(uf, x);
    int rootY = ufFind(uf, y);

    if (rootX == rootY) {
        return 0;  // Already in the same set
    }

    // Attach smaller rank tree under root of larger rank tree
    if (uf->rank[rootX] < uf->rank[rootY]) {
        uf->parent[rootX] = rootY;
    } else if (uf->rank[rootX] > uf->rank[rootY]) {
        uf->parent[rootY] = rootX;
    } else {
        uf->parent[rootY] = rootX;
        uf->rank[rootX]++;
    }
    return 1;
}

void ufFree(UnionFind* uf) {
    free(uf->parent);
    free(uf->rank);
    free(uf);
}

int main() {
    UnionFind* uf = ufCreate(5);

    ufUnion(uf, 0, 1);
    ufUnion(uf, 2, 3);
    ufUnion(uf, 0, 2);

    printf("find(0) = %d\n", ufFind(uf, 0));
    printf("find(1) = %d\n", ufFind(uf, 1));
    printf("find(2) = %d\n", ufFind(uf, 2));
    printf("find(3) = %d\n", ufFind(uf, 3));
    printf("find(4) = %d\n", ufFind(uf, 4));

    printf("0 and 3 in same set: %s\n",
        ufFind(uf, 0) == ufFind(uf, 3) ? "yes" : "no");
    printf("0 and 4 in same set: %s\n",
        ufFind(uf, 0) == ufFind(uf, 4) ? "yes" : "no");

    ufFree(uf);
    return 0;
}

Python 实现

class UnionFind:
    """Union-Find with path compression and union by rank."""

    def __init__(self, n):
        self.parent = list(range(n))  # Each node is its own parent
        self.rank = [0] * n

    def find(self, x):
        """Find with path compression."""
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])  # Path compression
        return self.parent[x]

    def union(self, x, y):
        """Union by rank, returns True if merged."""
        root_x = self.find(x)
        root_y = self.find(y)

        if root_x == root_y:
            return False  # Already in the same set

        # Attach smaller rank tree under root of larger rank tree
        if self.rank[root_x] < self.rank[root_y]:
            self.parent[root_x] = root_y
        elif self.rank[root_x] > self.rank[root_y]:
            self.parent[root_y] = root_x
        else:
            self.parent[root_y] = root_x
            self.rank[root_x] += 1
        return True

if __name__ == "__main__":
    uf = UnionFind(5)

    uf.union(0, 1)
    uf.union(2, 3)
    uf.union(0, 2)

    print(f"find(0) = {uf.find(0)}")
    print(f"find(1) = {uf.find(1)}")
    print(f"find(2) = {uf.find(2)}")
    print(f"find(3) = {uf.find(3)}")
    print(f"find(4) = {uf.find(4)}")

    print("0 and 3 in same set:", "yes" if uf.find(0) == uf.find(3) else "no")
    print("0 and 4 in same set:", "yes" if uf.find(0) == uf.find(4) else "no")

Go 实现

package main

import "fmt"

type UnionFind struct {
    parent []int
    rank   []int
}

func NewUnionFind(n int) *UnionFind {
    uf := &UnionFind{
        parent: make([]int, n),
        rank:   make([]int, n),
    }
    // Each node is its own parent initially
    for i := range uf.parent {
        uf.parent[i] = i
    }
    return uf
}

// Find with path compression
func (uf *UnionFind) Find(x int) int {
    if uf.parent[x] != x {
        uf.parent[x] = uf.Find(uf.parent[x]) // Path compression
    }
    return uf.parent[x]
}

// Union by rank, returns true if merged
func (uf *UnionFind) Union(x, y int) bool {
    rootX := uf.Find(x)
    rootY := uf.Find(y)

    if rootX == rootY {
        return false // Already in the same set
    }

    // Attach smaller rank tree under root of larger rank tree
    if uf.rank[rootX] < uf.rank[rootY] {
        uf.parent[rootX] = rootY
    } else if uf.rank[rootX] > uf.rank[rootY] {
        uf.parent[rootY] = rootX
    } else {
        uf.parent[rootY] = rootX
        uf.rank[rootX]++
    }
    return true
}

func main() {
    uf := NewUnionFind(5)

    uf.Union(0, 1)
    uf.Union(2, 3)
    uf.Union(0, 2)

    fmt.Printf("find(0) = %d\n", uf.Find(0))
    fmt.Printf("find(1) = %d\n", uf.Find(1))
    fmt.Printf("find(2) = %d\n", uf.Find(2))
    fmt.Printf("find(3) = %d\n", uf.Find(3))
    fmt.Printf("find(4) = %d\n", uf.Find(4))

    same03 := "no"
    if uf.Find(0) == uf.Find(3) {
        same03 = "yes"
    }
    fmt.Println("0 and 3 in same set:", same03)

    same04 := "no"
    if uf.Find(0) == uf.Find(4) {
        same04 = "yes"
    }
    fmt.Println("0 and 4 in same set:", same04)
}

上述代码实现了带路径压缩和按秩合并优化的并查集。find 函数在查找根节点的过程中,将沿途经过的所有节点直接指向根节点(路径压缩),使后续查询近乎 O(1)。union 函数在合并两个集合时,将秩较小的树挂在秩较大的树下(按秩合并),避免树退化为链表。初始化时每个节点自成一个集合(parent[i] = i)。测试演示了三次合并操作后各节点的归属关系。

运行该程序将输出:

find(0) = 0
find(1) = 0
find(2) = 0
find(3) = 0
find(4) = 4
0 and 3 in same set: yes
0 and 4 in same set: no

Kruskal 算法实现

Kruskal 算法的完整步骤如下:

  1. 将图中所有边按权重从小到大排序
  2. 初始化并查集,每个顶点各自为一个集合
  3. 按排序顺序遍历每条边 (u, v, weight):
    • 如果 u 和 v 属于不同的集合(find(u) != find(v)),则将这条边加入 MST,并合并 u 和 v 所在的集合
    • 如果 u 和 v 已在同一集合中,跳过该边(加入会形成环)
  4. 当 MST 包含 V-1 条边时停止
Kruskal 算法执行过程:

已排序的边(按权重):
(6,7,1) (2,8,2) (5,6,2) (0,1,4) (2,5,4) (6,8,6) (2,3,7) (7,8,7) (0,7,8) ...

逐步选边:
1. (6,7,1): find(6)!=find(7) → 加入MST, union(6,7)   MST总重=1
2. (2,8,2): find(2)!=find(8) → 加入MST, union(2,8)   MST总重=3
3. (5,6,2): find(5)!=find(6) → 加入MST, union(5,6)   MST总重=5
4. (0,1,4): find(0)!=find(1) → 加入MST, union(0,1)   MST总重=9
5. (2,5,4): find(2)!=find(5) → 加入MST, union(2,5)   MST总重=13
6. (6,8,6): find(6)==find(8) → 跳过(会形成环)
7. (2,3,7): find(2)!=find(3) → 加入MST, union(2,3)   MST总重=20
8. (7,8,7): find(7)==find(8) → 跳过(会形成环)
9. (0,7,8): find(0)!=find(7) → 加入MST, union(0,7)   MST总重=28
10.(3,4,9): find(3)!=find(4) → 加入MST, union(3,4)   MST总重=37

已选 8 条边 = V-1 = 9-1 = 8, 停止

MST 总权重 = 37

C++ 实现

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

struct Edge {
    int u, v, weight;
};

bool compareEdges(const Edge& a, const Edge& b) {
    return a.weight < b.weight;
}

class UnionFind {
private:
    vector<int> parent;
    vector<int> rnk;

public:
    UnionFind(int n) {
        parent.resize(n);
        rnk.resize(n, 0);
        for (int i = 0; i < n; i++) parent[i] = i;
    }

    int find(int x) {
        if (parent[x] != x) parent[x] = find(parent[x]);
        return parent[x];
    }

    bool unionSets(int x, int y) {
        int rx = find(x), ry = find(y);
        if (rx == ry) return false;
        if (rnk[rx] < rnk[ry]) parent[rx] = ry;
        else if (rnk[rx] > rnk[ry]) parent[ry] = rx;
        else { parent[ry] = rx; rnk[rx]++; }
        return true;
    }
};

void kruskal(int V, vector<Edge>& edges) {
    // Step 1: Sort edges by weight
    sort(edges.begin(), edges.end(), compareEdges);

    UnionFind uf(V);
    vector<Edge> mst;
    int totalWeight = 0;

    // Step 2: Iterate sorted edges
    for (const Edge& e : edges) {
        if (uf.unionSets(e.u, e.v)) {
            mst.push_back(e);
            totalWeight += e.weight;
        }
        // MST has V-1 edges
        if ((int)mst.size() == V - 1) break;
    }

    // Print result
    cout << "Edges in MST:" << endl;
    for (const Edge& e : mst) {
        cout << "  (" << e.u << "," << e.v << "," << e.weight << ")" << endl;
    }
    cout << "Total MST weight: " << totalWeight << endl;
}

int main() {
    int V = 9;
    vector<Edge> edges = {
        {0, 1, 4},  {0, 7, 8},  {1, 2, 8},  {1, 7, 11},
        {2, 3, 7},  {2, 5, 4},  {2, 8, 2},  {3, 4, 9},
        {3, 5, 14}, {4, 5, 10}, {5, 6, 2},  {6, 7, 1},
        {6, 8, 6},  {7, 8, 7},
    };

    kruskal(V, edges);

    return 0;
}

C 实现

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

typedef struct {
    int u, v, weight;
} Edge;

typedef struct {
    int* parent;
    int* rank;
    int n;
} UnionFind;

UnionFind* ufCreate(int n) {
    UnionFind* uf = (UnionFind*)malloc(sizeof(UnionFind));
    uf->n = n;
    uf->parent = (int*)malloc(n * sizeof(int));
    uf->rank = (int*)calloc(n, sizeof(int));
    for (int i = 0; i < n; i++) uf->parent[i] = i;
    return uf;
}

int ufFind(UnionFind* uf, int x) {
    if (uf->parent[x] != x) uf->parent[x] = ufFind(uf, uf->parent[x]);
    return uf->parent[x];
}

int ufUnion(UnionFind* uf, int x, int y) {
    int rx = ufFind(uf, x), ry = ufFind(uf, y);
    if (rx == ry) return 0;
    if (uf->rank[rx] < uf->rank[ry]) uf->parent[rx] = ry;
    else if (uf->rank[rx] > uf->rank[ry]) uf->parent[ry] = rx;
    else { uf->parent[ry] = rx; uf->rank[rx]++; }
    return 1;
}

void ufFree(UnionFind* uf) {
    free(uf->parent);
    free(uf->rank);
    free(uf);
}

int compareEdges(const void* a, const void* b) {
    return ((Edge*)a)->weight - ((Edge*)b)->weight;
}

void kruskal(int V, Edge edges[], int edgeCount) {
    // Step 1: Sort edges by weight
    qsort(edges, edgeCount, sizeof(Edge), compareEdges);

    UnionFind* uf = ufCreate(V);
    Edge mst[14];
    int mstCount = 0;
    int totalWeight = 0;

    // Step 2: Iterate sorted edges
    for (int i = 0; i < edgeCount; i++) {
        if (ufUnion(uf, edges[i].u, edges[i].v)) {
            mst[mstCount++] = edges[i];
            totalWeight += edges[i].weight;
        }
        if (mstCount == V - 1) break;
    }

    // Print result
    printf("Edges in MST:\n");
    for (int i = 0; i < mstCount; i++) {
        printf("  (%d,%d,%d)\n", mst[i].u, mst[i].v, mst[i].weight);
    }
    printf("Total MST weight: %d\n", totalWeight);

    ufFree(uf);
}

int main() {
    int V = 9;
    Edge edges[] = {
        {0, 1, 4},  {0, 7, 8},  {1, 2, 8},  {1, 7, 11},
        {2, 3, 7},  {2, 5, 4},  {2, 8, 2},  {3, 4, 9},
        {3, 5, 14}, {4, 5, 10}, {5, 6, 2},  {6, 7, 1},
        {6, 8, 6},  {7, 8, 7},
    };
    int edgeCount = sizeof(edges) / sizeof(edges[0]);

    kruskal(V, edges, edgeCount);

    return 0;
}

Python 实现

class UnionFind:
    """Union-Find with path compression and union by rank."""

    def __init__(self, n):
        self.parent = list(range(n))
        self.rank = [0] * n

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        root_x = self.find(x)
        root_y = self.find(y)
        if root_x == root_y:
            return False
        if self.rank[root_x] < self.rank[root_y]:
            self.parent[root_x] = root_y
        elif self.rank[root_x] > self.rank[root_y]:
            self.parent[root_y] = root_x
        else:
            self.parent[root_y] = root_x
            self.rank[root_x] += 1
        return True

def kruskal(V, edges):
    """Kruskal's algorithm for Minimum Spanning Tree."""
    # Step 1: Sort edges by weight
    edges.sort(key=lambda e: e[2])

    uf = UnionFind(V)
    mst = []
    total_weight = 0

    # Step 2: Iterate sorted edges
    for u, v, w in edges:
        if uf.union(u, v):
            mst.append((u, v, w))
            total_weight += w
        if len(mst) == V - 1:
            break

    # Print result
    print("Edges in MST:")
    for u, v, w in mst:
        print(f"  ({u},{v},{w})")
    print(f"Total MST weight: {total_weight}")

if __name__ == "__main__":
    V = 9
    edges = [
        (0, 1, 4),  (0, 7, 8),  (1, 2, 8),  (1, 7, 11),
        (2, 3, 7),  (2, 5, 4),  (2, 8, 2),  (3, 4, 9),
        (3, 5, 14), (4, 5, 10), (5, 6, 2),  (6, 7, 1),
        (6, 8, 6),  (7, 8, 7),
    ]

    kruskal(V, edges)

Go 实现

package main

import (
    "fmt"
    "sort"
)

type Edge struct {
    U, V, Weight int
}

type UnionFind struct {
    parent []int
    rank   []int
}

func NewUnionFind(n int) *UnionFind {
    uf := &UnionFind{
        parent: make([]int, n),
        rank:   make([]int, n),
    }
    for i := range uf.parent {
        uf.parent[i] = i
    }
    return uf
}

func (uf *UnionFind) Find(x int) int {
    if uf.parent[x] != x {
        uf.parent[x] = uf.Find(uf.parent[x])
    }
    return uf.parent[x]
}

func (uf *UnionFind) Union(x, y int) bool {
    rx, ry := uf.Find(x), uf.Find(y)
    if rx == ry {
        return false
    }
    if uf.rank[rx] < uf.rank[ry] {
        uf.parent[rx] = ry
    } else if uf.rank[rx] > uf.rank[ry] {
        uf.parent[ry] = rx
    } else {
        uf.parent[ry] = rx
        uf.rank[rx]++
    }
    return true
}

func kruskal(V int, edges []Edge) {
    // Step 1: Sort edges by weight
    sort.Slice(edges, func(i, j int) bool {
        return edges[i].Weight < edges[j].Weight
    })

    uf := NewUnionFind(V)
    var mst []Edge
    totalWeight := 0

    // Step 2: Iterate sorted edges
    for _, e := range edges {
        if uf.Union(e.U, e.V) {
            mst = append(mst, e)
            totalWeight += e.Weight
        }
        if len(mst) == V-1 {
            break
        }
    }

    // Print result
    fmt.Println("Edges in MST:")
    for _, e := range mst {
        fmt.Printf("  (%d,%d,%d)\n", e.U, e.V, e.Weight)
    }
    fmt.Printf("Total MST weight: %d\n", totalWeight)
}

func main() {
    V := 9
    edges := []Edge{
        {0, 1, 4},  {0, 7, 8},  {1, 2, 8},  {1, 7, 11},
        {2, 3, 7},  {2, 5, 4},  {2, 8, 2},  {3, 4, 9},
        {3, 5, 14}, {4, 5, 10}, {5, 6, 2},  {6, 7, 1},
        {6, 8, 6},  {7, 8, 7},
    }

    kruskal(V, edges)
}

上述代码实现了完整的 Kruskal 最小生成树算法。首先将所有边按权重排序,然后使用并查集依次检查每条边:如果边的两个端点属于不同的集合,说明加入该边不会形成环,将其加入 MST 并合并两个集合;如果两个端点已在同一集合中,则跳过该边。当 MST 中包含 V-1 条边时算法结束。四种语言的实现逻辑完全一致,区别仅在于语言特性:C++ 使用结构体和类;C 语言使用函数和手动内存管理;Python 使用类和列表;Go 使用结构体和方法。

运行该程序将输出:

Edges in MST:
  (6,7,1)
  (2,8,2)
  (5,6,2)
  (0,1,4)
  (2,5,4)
  (2,3,7)
  (0,7,8)
  (3,4,9)
Total MST weight: 37

Kruskal 算法的性质

下表总结了 Kruskal 算法的时间和空间复杂度:

指标 复杂度 说明
时间复杂度(Time Complexity) O(E log E) 排序 O(E log E),并查集操作 O(E alpha(V)),整体由排序主导
空间复杂度(Space Complexity) O(V + E) 存储边列表 O(E),并查集 O(V),MST 结果 O(V)

其中 V 是顶点数(Vertex Count),E 是边数(Edge Count),alpha 是反阿克曼函数(Inverse Ackermann Function)。

Kruskal 算法的关键性质:

  • 贪心正确性:Kruskal 算法基于贪心策略,每次选择权重最小且不形成环的边。该贪心策略的正确性由 cut property(切割性质)保证:对于图的任意一个割(Cut),横跨该割的最小权重边一定属于某棵最小生成树。
  • 基于边而非顶点:Kruskal 从边的角度出发,不依赖起始顶点的选择,适合边数较少的稀疏图(Sparse Graph)。
  • 环检测是核心:Kruskal 的关键操作是判断加入一条边是否会形成环。并查集通过判断两个端点是否在同一集合来高效完成这一操作。
  • 结果唯一性:最小生成树的总权重是唯一的,但如果图中存在权重相同的边,可能有多棵不同的最小生成树。

Kruskal 与 Prim 算法的对比:

特性 Kruskal Prim
策略 按边权排序,贪心选边 从一个顶点出发,贪心扩展
数据结构 并查集(Union-Find) 优先队列(Priority Queue)
时间复杂度 O(E log E) O((V+E) log V)
适用场景 稀疏图(Sparse Graph) 稠密图(Dense Graph)
实现方式 基于边 基于顶点

Kruskal 算法的典型应用场景:

应用场景 说明
网络设计(Network Design) 设计成本最低的通信网络、管道网络
集群分析(Cluster Analysis) 单链接聚类(Single-linkage Clustering)的基础
电路设计(Circuit Design) 最小化电路布线的总长度
逼近旅行商问题(TSP Approximation) MST 权重是 TSP 最优解的下界
posted @ 2026-04-17 07:56  游翔  阅读(7)  评论(0)    收藏  举报