目录
1. 什么是最小生成树?
2. Kruskal算法
1. 什么是最小生成树?
最小生成树(Minimum Spanning Tree,简称 MST)是指在一个连通的无向图中,找到一个包含所有顶点的树,并且边的权值之和最小。
连通图:是指图中任意两个顶点之间都存在路径的图。
- 只能使用图中的边来构造最小生成树
- 只能使用恰好n-1条边来连接图中的n个顶点
- 选用的n-1条边不能构成回路
2. Kruskal算法
算法描述:
- 任给n个顶点;
- 首先构造一个由这n个顶点组成、不含任何边的图,其中每个顶点自成一个连通分量;
- 以顶点的每条边的权值排序(小->大),依次遍历排序后的边,对于每条边 (u, v),如果将其加入最小生成树不会形成回路,则将其加入图/最小生成树,并将顶点 u 和 v 合并为一个连通分量;
- 如此重复,直到该图/最小生成树包含了图中的所有顶点。
直接拿例题中的示例模拟一下过程:
1584. 连接所有点的最小费用
给你一个
points
数组,表示 2D 平面上的一些点,其中points[i] = [xi, yi]
。连接点
[xi, yi]
和点[xj, yj]
的费用为它们之间的 曼哈顿距离 :|xi - xj| + |yi - yj|
,其中|val|
表示val
的绝对值。请你返回将所有点连接的最小总费用。只有任意两点之间 有且仅有 一条简单路径时,才认为所有点都已连接。
根据算法原理进行分析:
- 该图有5个顶点,任意一个顶点会有4条边,对于无向图来说A-B和B-A是同一条边。
- 每个顶点是一个连通分量,我们先不管它。
- 要对所有边进行排序,不断选取权值最小的一条,对于这一步,我们需要将所有边保存起来,并且不能重复存边(如A-B,B-A只能存一次)。
- 每次选取的边,如果加入后,图中不会形成环(即这条边的两个顶点不能已经是一个连通块),则加入该条边,并将两个顶点合并成一个连通块。否则往后选取。
- 反复上述操作,直到找到n-1条边,就找到了最小生成树。
本题的代码实现:
- 边与顶点的关系,用一个类edge进行描述。
- 边的排序,可以用ArrayList,我这里用的是优先级队列。
- 维护每个顶点之间的连通关系,这里最好就是使用并查集。【并查集】一种简单而强大高效的数据结构
import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
public class LeetCode1584 {
//1584. 连接所有点的最小费用
static class Edge {
private int len; //权值
private int x; //顶点
private int y; //顶点
public Edge(int len, int x, int y) {
this.len = len;
this.x = x;
this.y = y;
}
}
public int minCostConnectPoints(int[][] points) {
int n = points.length;
//1.将连通图的所有边加入到优先级队列(以权值建小根堆)
PriorityQueue<Edge> pq = new PriorityQueue<>(Comparator.comparingInt(o -> o.len));
for (int i = 0; i < n; i++) {
//不要重复存边
for (int j = i + 1; j < n; j++) {
int x = Math.abs(points[j][0] - points[i][0]);
int y = Math.abs(points[j][1] - points[i][1]);
pq.offer(new Edge((x + y), i, j));
}
}
//2.贪心策略选边,并把选到边的对应两个顶点合并成一个连通块
int count = 0;
UnionFindSet ufs = new UnionFindSet(n);
int sum = 0;
while (!pq.isEmpty()) {
Edge edge = pq.poll();
int len = edge.len, x = edge.x, y = edge.y;
//如果两个顶点不构成连通块,将结果加上这条边,并合并两个顶点
if (ufs.union(x, y)) {
sum += len;
//n个顶点,选n - 1条边
if (++count == n - 1) {
break;
}
}
}
return sum;
}
public static void main(String[] args) {
System.out.println(new LeetCode1584().minCostConnectPoints(new int[][]{
{0, 0}, {2, 2}, {3, 10}, {5, 2}, {7, 0}
}));
}
}
class UnionFindSet {
//elem的值表示:如果大于0,表示这个点的父节点下标;如果小于0,表示该点是一个集合
private int[] elem;
public UnionFindSet(int n) { //参数n表示顶点个数
this.elem = new int[n];
Arrays.fill(elem, -1);
}
//合并两个顶点
public boolean union(int x, int y) {
x = find(x);
y = find(y);
//两个顶点已经是在同一个集合里了
if (x == y) return false;
elem[x] += elem[y];
elem[y] = x;
return true;
}
private int find(int x) {
while (elem[x] >= 0) {
x = elem[x];
}
return x;
}
}
首先创建了一个小根堆优先队列,用来存储所有的边,并按照边的权值从小到大排序。然后通过贪心策略选取边,确保选择的边不会形成环路,并且合并相应的顶点,直到选取了足够数量的边,形成了最小生成树。
同时,还实现了一个并查集(UnionFindSet)来维护顶点之间的连通性,这是 Kruskal 算法的关键之一。这里的并查集并不用实现很多功能,根据题目需求自行调整代码逻辑,这样的效率更高。比如在合并的时候,就判断两个顶点是否已在同一个连通块,针对其返回值确定是否要加入最小生成树。