简介
最小生成树(Minimum Spanning Tree,MST)是指在一个加权连通图中,连接所有顶点的边权值之和最小的生成树。Prim算法和Kruskal算法是两种常用的求解最小生成树的算法。
Prim算法
Prim算法是一种贪心算法,它从图中任意一个顶点开始,每次选择一条权值最小的边将新的顶点加入到生成树中,直到所有顶点都被加入。
算法步骤
- 初始化一个集合S,其中包含一个任意顶点。
- 将图中所有边按照权值从小到大排序。
- 从排序后的边中依次取出边,如果该边的两个顶点不在集合S中,则将该边加入到生成树中,并将该边的两个顶点加入到集合S中。
- 重复步骤3,直到集合S包含所有顶点。
Kruskal算法
Kruskal算法也是一种贪心算法,它将图中所有边按照权值从小到大排序,然后从权值最小的边开始,每次选择一条不构成环路的边加入到生成树中,直到所有顶点都被加入。
特点
Kruskal算法具有以下特点:
- Kruskal算法也是一种贪心算法,它每次选择不构成环路的边,因此可以找到最小生成树。
- Kruskal算法的时间复杂度为 O(E log E)。
- Kruskal算法的空间复杂度为 O(E)
算法步骤
- 将图中所有边按照权值从小到大排序。
- 初始化一个并查集,其中每个顶点代表一个集合。
- 从排序后的边中依次取出边,如果该边的两个顶点属于不同的集合,则将该边加入到生成树中,并将该边的两个顶点所在的集合合并。
- 重复步骤3,直到所有顶点属于同一个集合。
图片来自知乎大佬@TeddyZhang
由于Kruksal算法是对边进行操作,先取出边,然后判断边的两个节点,这样的话,如果一个图结构非常的稠密,那么Kruksal算法就比较慢了,而Prim算法只是对节点进行遍历,并使用set进行标记,因此会相对于Kruksal算法,在稠密图方面好很多,因此Kruksal算法常用于稀疏图,而Prim算法常用于稠密图!
题目描述
如题,给出一个无向图,求出最小生成树,如果该图不连通,则输出 `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) # 输出最小生成树的总权值