最小生成树:Prim算法和Kruskal算法

本文介绍了最小生成树的概念,重点讲解了Prim算法和Kruskal算法的工作原理、步骤,以及它们在不同图结构中的适用性,特别是Prim算法在稠密图和Kruskal算法在稀疏图中的优势。同时提供了C、Python代码示例来求解最小生成树问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简介

最小生成树(Minimum Spanning Tree,MST)是指在一个加权连通图中,连接所有顶点的边权值之和最小的生成树。Prim算法和Kruskal算法是两种常用的求解最小生成树的算法。

Prim算法

Prim算法是一种贪心算法,它从图中任意一个顶点开始,每次选择一条权值最小的边将新的顶点加入到生成树中,直到所有顶点都被加入。

算法步骤

  1. 初始化一个集合S,其中包含一个任意顶点。
  2. 将图中所有边按照权值从小到大排序。
  3. 从排序后的边中依次取出边,如果该边的两个顶点不在集合S中,则将该边加入到生成树中,并将该边的两个顶点加入到集合S中。
  4. 重复步骤3,直到集合S包含所有顶点。

Kruskal算法

Kruskal算法也是一种贪心算法,它将图中所有边按照权值从小到大排序,然后从权值最小的边开始,每次选择一条不构成环路的边加入到生成树中,直到所有顶点都被加入。

特点

Kruskal算法具有以下特点:

  • Kruskal算法也是一种贪心算法,它每次选择不构成环路的边,因此可以找到最小生成树。
  • Kruskal算法的时间复杂度为 O(E log E)。
  • Kruskal算法的空间复杂度为 O(E)

算法步骤

  1. 将图中所有边按照权值从小到大排序。
  2. 初始化一个并查集,其中每个顶点代表一个集合。
  3. 从排序后的边中依次取出边,如果该边的两个顶点属于不同的集合,则将该边加入到生成树中,并将该边的两个顶点所在的集合合并。
  4. 重复步骤3,直到所有顶点属于同一个集合。

图片来自知乎大佬@TeddyZhang

由于Kruksal算法是对边进行操作,先取出边,然后判断边的两个节点,这样的话,如果一个图结构非常的稠密,那么Kruksal算法就比较慢了,而Prim算法只是对节点进行遍历,并使用set进行标记,因此会相对于Kruksal算法,在稠密图方面好很多,因此Kruksal算法常用于稀疏图,而Prim算法常用于稠密图!

例题P3366 【模板】最小生成树

 题目描述

如题,给出一个无向图,求出最小生成树,如果该图不连通,则输出 `orz`。

 输入格式

第一行包含两个整数 N,M,表示该图共有 N个结点和M条无向边。

接下来 M 行每行包含三个整数 X,Y,Z,表示有一条长度为 Z的无向边连接结点 X,Y。

输出格式

如果该图连通,则输出一个整数表示最小生成树的各边的长度之和。如果该图不连通则输出 `orz`。

prime代码实现 c

#include <stdio.h>
#include <stdbool.h>
#include <string.h>

#define MAX_N 1005 // 最大点数
#define MAX_M 20005 // 最大边数(无向图)
#define INF 0x3f3f3f3f // 无穷大

typedef struct {
    int v, w; // 边的终点和权值
} Edge;

Edge edges[MAX_M]; // 存储所有边
int head[MAX_N], nxt[MAX_M], idx; // 邻接表存储方式,head 存储每个点的第一条边的下标,nxt 存储相同起点的下一条边的下标,idx 维护当前边的下标
int dist[MAX_N]; // 存储每个点到最小生成树的距离
bool vis[MAX_N]; // 标记每个点是否已经加入最小生成树

// 添加一条边 u -> v,权值为 w
void addEdge(int u, int v, int w) {
    edges[idx].v = v;
    edges[idx].w = w;
    nxt[idx] = head[u];
    head[u] = idx++;
}

// Prim 最小生成树算法
int prim(int n) {
    // 初始化 dist 和 vis 数组
    for (int i = 1; i <= n; i++) {
        dist[i] = INF;
        vis[i] = false;
    }

    dist[1] = 0; // 假设 1 号节点为起点,将其距离设为 0
    int ans = 0; // 维护最小生成树的总权值

    // 构建最小生成树
    for (int i = 1; i <= n; i++) {
        int u = -1, minDist = INF;
        // 找到距离最小生成树最近的一个未加入最小生成树的点 u
        for (int j = 1; j <= n; j++) {
            if (!vis[j] && dist[j] < minDist) {
                u = j;
                minDist = dist[j];
            }
        }

        if (u == -1) return -1; // 图不连通

        vis[u] = true; // 将 u 加入最小生成树
        ans += minDist; // 更新最小生成树的总权值

        // 更新其他点到最小生成树的距离
        for (int j = head[u]; j != -1; j = nxt[j]) {
            int v = edges[j].v, w = edges[j].w;
            if (!vis[v] && dist[v] > w) {
                dist[v] = w;
            }
        }
    }

    return ans; // 返回最小生成树的总权值
}

int main() {
    int n, m;
    scanf("%d %d", &n, &m); // 输入节点数和边数
    memset(head, -1, sizeof(head)); // 初始化邻接表

    // 输入每条边的起点、终点和权值,并将其添加到邻接表中
    for (int i = 0; i < m; i++) {
        int u, v, w;
        scanf("%d %d %d", &u, &v, &w);
        addEdge(u, v, w);
        addEdge(v, u, w); // 由于是无向图,需要添加两条边
    }

    int res = prim(n); // 调用 Prim 算法求解最小生成树的总权值
    if (res == -1) {
        printf("orz\n"); // 图不连通
    }
    else {
        printf("%d\n", res); // 输出最小生成树的总权值
    }

    return 0;
}

Python代码

import heapq

INF = 0x3f3f3f3f  # 定义无穷大的值

def prim(n, edges):
    # 构建邻接表表示的图
    graph = {i: [] for i in range(1, n + 1)}
    for u, v, w in edges:
        graph[u].append((v, w))
        graph[v].append((u, w))

    visited = [False] * (n + 1)  # 标记节点是否被访问过
    dist = [INF] * (n + 1)  # 记录从生成树到各个节点的最短距离
    dist[1] = 0  # 初始时,将第一个节点加入生成树
    pq = [(0, 1)]  # 使用优先队列来快速找到最短距离的节点,初始时放入第一个节点
    ans = 0  # 最小生成树的总权值

    while pq:
        d, u = heapq.heappop(pq)  # 弹出当前最短距离的节点
        if visited[u]:
            continue
        visited[u] = True  # 标记该节点为已访问
        ans += d  # 将该节点到生成树的距离累加到结果中
        for v, w in graph[u]:  # 遍历与当前节点相连的节点
            if not visited[v] and w < dist[v]:  # 如果该节点未被访问且到生成树的距离比当前记录的距离小
                dist[v] = w  # 更新到生成树的最短距离
                heapq.heappush(pq, (w, v))  # 将该节点加入优先队列

    return ans if all(visited[1:]) else -1  # 若所有节点都被访问到了,返回最小生成树的总权值,否则返回-1表示图不连通


def main():
    n, m = map(int, input().split())  # 读取节点数和边数
    edges = [list(map(int, input().split())) for _ in range(m)]  # 读取每条边的起点、终点和权值
    res = prim(n, edges)  # 计算最小生成树的总权值
    if res == -1:
        print("orz")
    else:
        print(res)


if __name__ == "__main__":
    main()

Kruskal c代码

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

#define MAX_N 1005 // 最大点数
#define MAX_M 20005 // 最大边数(无向图)

typedef struct {
    int u, v, w; // 边的起点、终点和权值
} Edge;

Edge edges[MAX_M]; // 存储所有边
int parent[MAX_N]; // 并查集数组,用于判断是否形成环

// 并查集的查找根节点操作
int find(int x) {
    if (parent[x] != x) {
        parent[x] = find(parent[x]);
    }
    return parent[x];
}

// 并查集的合并操作
void unionSet(int x, int y) {
    int rootX = find(x);
    int rootY = find(y);
    if (rootX != rootY) {
        parent[rootX] = rootY;
    }
}

// Kruskal 最小生成树算法
int kruskal(int n, int m) {
    // 初始化并查集
    for (int i = 1; i <= n; i++) {
        parent[i] = i;
    }

    // 对所有边按权值进行升序排序
    qsort(edges, m, sizeof(Edge),
        [](const void* a, const void* b) {
            return ((Edge*)a)->w - ((Edge*)b)->w;
        });

    int ans = 0; // 最小生成树的总权值

    // 遍历每条边
    for (int i = 0; i < m; i++) {
        int u = edges[i].u, v = edges[i].v, w = edges[i].w;

        // 如果边的起点和终点不在同一个连通分量中,则加入最小生成树
        if (find(u) != find(v)) {
            ans += w;
            unionSet(u, v);
        }
    }

    return ans;
}

int main() {
    int n, m;
    scanf("%d%d", &n, &m); // 输入节点数和边数

    // 输入每条边的起点、终点和权值
    for (int i = 0; i < m; i++) {
        scanf("%d %d %d", &edges[i].u, &edges[i].v, &edges[i].w);
    }

    int res = kruskal(n, m); // 调用 Kruskal 算法求解最小生成树的总权值
    printf("%d\n", res); // 输出最小生成树的总权值

    return 0;
}

Python代码

class DisjointSet:
    def __init__(self, n):
        self.parent = [i for i in range(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:
            self.parent[root_x] = root_y


def kruskal(n, edges):
    ds = DisjointSet(n)
    edges.sort(key=lambda x: x[2])  # 按权值排序边

    ans = 0  # 最小生成树的总权值
    for u, v, w in edges:
        if ds.find(u - 1) != ds.find(v - 1):  # 如果起点和终点不在同一个连通分量中
            ans += w
            ds.union(u - 1, v - 1)

    return ans


if __name__ == "__main__":
    n, m = map(int, input().split())  # 输入节点数和边数
    edges = [list(map(int, input().split())) for _ in range(m)]  # 输入每条边的起点、终点和权值

    res = kruskal(n, edges)  # 调用 Kruskal 算法求解最小生成树的总权值
    print(res)  # 输出最小生成树的总权值

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值